merge integration

This commit is contained in:
jess 2026-03-31 13:30:50 -07:00
commit 3ce7aa2fdf
9 changed files with 564 additions and 77 deletions

View File

@ -10,3 +10,6 @@ categories = ["mathematics", "no-std"]
[dependencies] [dependencies]
cord-trig = { path = "../cord-trig" } cord-trig = { path = "../cord-trig" }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
regex = "1"

View File

@ -1,4 +1,6 @@
use std::collections::BTreeMap;
use cord_trig::{TrigGraph, TrigOp}; use cord_trig::{TrigGraph, TrigOp};
use crate::config::{CordicConfig, CordicOp};
use crate::ops::*; use crate::ops::*;
/// A compiled CORDIC program ready for binary serialization or execution. /// A compiled CORDIC program ready for binary serialization or execution.
@ -6,11 +8,29 @@ use crate::ops::*;
/// Each instruction produces its result into a slot matching its index. /// Each instruction produces its result into a slot matching its index.
/// The instruction list parallels the TrigGraph node list — compilation /// The instruction list parallels the TrigGraph node list — compilation
/// is a direct 1:1 mapping with constants folded to fixed-point. /// is a direct 1:1 mapping with constants folded to fixed-point.
///
/// Supports per-operation iteration counts via `tables`: each unique
/// iteration count gets its own atan table and gain constant. Instructions
/// index into `tables` via `instr_table_idx`.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CORDICProgram { pub struct CORDICProgram {
pub word_bits: u8, pub word_bits: u8,
pub instructions: Vec<CORDICInstr>, pub instructions: Vec<CORDICInstr>,
pub output: u32, pub output: u32,
/// One entry per unique iteration count.
pub tables: Vec<CordicTable>,
/// Maps instruction index → table index. Only meaningful for CORDIC ops;
/// non-CORDIC instructions have 0 here (ignored at eval time).
pub instr_table_idx: Vec<u8>,
/// Legacy fields for backward compat with single-table programs.
pub atan_table: Vec<i64>,
pub gain: i64,
}
/// Precomputed atan table + gain for a specific iteration count.
#[derive(Debug, Clone)]
pub struct CordicTable {
pub iterations: u8,
pub atan_table: Vec<i64>, pub atan_table: Vec<i64>,
pub gain: i64, pub gain: i64,
} }
@ -18,11 +38,12 @@ pub struct CORDICProgram {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CompileConfig { pub struct CompileConfig {
pub word_bits: u8, pub word_bits: u8,
pub cordic: Option<CordicConfig>,
} }
impl Default for CompileConfig { impl Default for CompileConfig {
fn default() -> Self { fn default() -> Self {
Self { word_bits: 32 } Self { word_bits: 32, cordic: None }
} }
} }
@ -30,15 +51,80 @@ impl CORDICProgram {
/// Compile a TrigGraph into a CORDIC program. /// Compile a TrigGraph into a CORDIC program.
/// ///
/// Each TrigOp node becomes one CORDICInstr. Constants are converted /// Each TrigOp node becomes one CORDICInstr. Constants are converted
/// from f64 to fixed-point at compile time. Everything else is a /// from f64 to fixed-point at compile time. Per-operation iteration
/// direct structural mapping. /// counts are resolved from the CordicConfig if present, otherwise
/// all ops use word_bits as their iteration count.
pub fn compile(graph: &TrigGraph, config: &CompileConfig) -> Self { pub fn compile(graph: &TrigGraph, config: &CompileConfig) -> Self {
let frac_bits = config.word_bits - 1; let frac_bits = config.word_bits - 1;
let to_fixed = |val: f64| -> i64 { let to_fixed = |val: f64| -> i64 {
(val * (1i64 << frac_bits) as f64).round() as i64 (val * (1i64 << frac_bits) as f64).round() as i64
}; };
let cordic_cfg = config.cordic.as_ref().map(|c| c.clone())
.unwrap_or_else(|| {
let mut c = CordicConfig::default();
c.word_bits = config.word_bits;
c.default_f = config.word_bits;
c
});
// Build tables for each unique iteration count
let unique_counts = cordic_cfg.unique_iteration_counts();
let mut count_to_table_idx: BTreeMap<u8, u8> = BTreeMap::new();
let mut tables = Vec::new();
for &iters in &unique_counts {
count_to_table_idx.insert(iters, tables.len() as u8);
tables.push(CordicTable {
iterations: iters,
atan_table: atan_table(iters),
gain: cordic_gain(iters, frac_bits),
});
}
let default_table_idx = count_to_table_idx
.get(&cordic_cfg.default_f)
.copied()
.unwrap_or_else(|| {
// Ensure default has a table entry
let idx = tables.len() as u8;
tables.push(CordicTable {
iterations: cordic_cfg.default_f,
atan_table: atan_table(cordic_cfg.default_f),
gain: cordic_gain(cordic_cfg.default_f, frac_bits),
});
idx
});
let resolve_table = |cop: CordicOp| -> u8 {
let iters = cordic_cfg.iterations_for(cop);
count_to_table_idx.get(&iters).copied().unwrap_or(default_table_idx)
};
let mut instr_table_idx = Vec::with_capacity(graph.nodes.len());
let instructions: Vec<CORDICInstr> = graph.nodes.iter().map(|op| { let instructions: Vec<CORDICInstr> = graph.nodes.iter().map(|op| {
let tidx = match op {
TrigOp::Sin(_) => resolve_table(CordicOp::Sin),
TrigOp::Cos(_) => resolve_table(CordicOp::Cos),
TrigOp::Tan(_) => resolve_table(CordicOp::Tan),
TrigOp::Asin(_) => resolve_table(CordicOp::Asin),
TrigOp::Acos(_) => resolve_table(CordicOp::Acos),
TrigOp::Atan(_) => resolve_table(CordicOp::Atan),
TrigOp::Sinh(_) => resolve_table(CordicOp::Sinh),
TrigOp::Cosh(_) => resolve_table(CordicOp::Cosh),
TrigOp::Tanh(_) => resolve_table(CordicOp::Tanh),
TrigOp::Asinh(_) => resolve_table(CordicOp::Asinh),
TrigOp::Acosh(_) => resolve_table(CordicOp::Acosh),
TrigOp::Atanh(_) => resolve_table(CordicOp::Atanh),
TrigOp::Sqrt(_) => resolve_table(CordicOp::Sqrt),
TrigOp::Exp(_) => resolve_table(CordicOp::Exp),
TrigOp::Ln(_) => resolve_table(CordicOp::Ln),
TrigOp::Hypot(_, _) => resolve_table(CordicOp::Hypot),
TrigOp::Atan2(_, _) => resolve_table(CordicOp::Atan2),
_ => 0,
};
instr_table_idx.push(tidx);
match op { match op {
TrigOp::InputX => CORDICInstr::InputX, TrigOp::InputX => CORDICInstr::InputX,
TrigOp::InputY => CORDICInstr::InputY, TrigOp::InputY => CORDICInstr::InputY,
@ -77,12 +163,21 @@ impl CORDICProgram {
} }
}).collect(); }).collect();
// Legacy single-table fields use the first table (or default)
let primary = tables.first().cloned().unwrap_or_else(|| CordicTable {
iterations: config.word_bits,
atan_table: atan_table(config.word_bits),
gain: cordic_gain(config.word_bits, frac_bits),
});
CORDICProgram { CORDICProgram {
word_bits: config.word_bits, word_bits: config.word_bits,
instructions, instructions,
output: graph.output, output: graph.output,
atan_table: atan_table(config.word_bits), tables,
gain: cordic_gain(config.word_bits, frac_bits), instr_table_idx,
atan_table: primary.atan_table,
gain: primary.gain,
} }
} }
@ -153,6 +248,15 @@ impl CORDICProgram {
pos += consumed; pos += consumed;
} }
Some(CORDICProgram { word_bits, instructions, output, atan_table, gain }) let tables = vec![CordicTable {
iterations: word_bits,
atan_table: atan_table.clone(),
gain,
}];
let instr_table_idx = vec![0u8; instructions.len()];
Some(CORDICProgram {
word_bits, instructions, output, tables, instr_table_idx, atan_table, gain,
})
} }
} }

View File

@ -0,0 +1,305 @@
use std::collections::BTreeMap;
use cord_trig::{TrigGraph, TrigOp};
use serde::{Deserialize, Serialize};
/// Canonical name for each CORDIC-consuming operation.
/// Non-CORDIC ops (Add, Sub, etc.) don't appear here.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CordicOp {
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
Sinh,
Cosh,
Tanh,
Asinh,
Acosh,
Atanh,
Sqrt,
Exp,
Ln,
Hypot,
Atan2,
}
impl CordicOp {
pub fn name(&self) -> &'static str {
match self {
Self::Sin => "sin",
Self::Cos => "cos",
Self::Tan => "tan",
Self::Asin => "asin",
Self::Acos => "acos",
Self::Atan => "atan",
Self::Sinh => "sinh",
Self::Cosh => "cosh",
Self::Tanh => "tanh",
Self::Asinh => "asinh",
Self::Acosh => "acosh",
Self::Atanh => "atanh",
Self::Sqrt => "sqrt",
Self::Exp => "exp",
Self::Ln => "ln",
Self::Hypot => "hypot",
Self::Atan2 => "atan2",
}
}
pub fn all() -> &'static [CordicOp] {
&[
Self::Sin, Self::Cos, Self::Tan,
Self::Asin, Self::Acos, Self::Atan,
Self::Sinh, Self::Cosh, Self::Tanh,
Self::Asinh, Self::Acosh, Self::Atanh,
Self::Sqrt, Self::Exp, Self::Ln,
Self::Hypot, Self::Atan2,
]
}
pub fn from_name(s: &str) -> Option<Self> {
Self::all().iter().find(|op| op.name() == s).copied()
}
}
/// On-disk TOML representation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CordicConfigFile {
#[serde(default)]
pub defaults: Defaults,
#[serde(default, rename = "override")]
pub overrides: Vec<Override>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Defaults {
#[serde(default = "default_word_bits")]
pub word_bits: u8,
#[serde(default = "default_f")]
pub f: u8,
}
impl Default for Defaults {
fn default() -> Self {
Self {
word_bits: default_word_bits(),
f: default_f(),
}
}
}
fn default_word_bits() -> u8 { 32 }
fn default_f() -> u8 { 32 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Override {
/// Regex pattern matching operation names (e.g. "sin|cos", "sinh|cosh|tanh")
pub op: String,
/// Iteration count override
pub f: u8,
}
/// Resolved configuration: each operation has a concrete iteration count.
#[derive(Debug, Clone)]
pub struct CordicConfig {
pub word_bits: u8,
pub default_f: u8,
/// Per-operation iteration counts. Only contains ops actually used.
pub ops: BTreeMap<CordicOp, u8>,
}
impl Default for CordicConfig {
fn default() -> Self {
Self {
word_bits: 32,
default_f: 32,
ops: BTreeMap::new(),
}
}
}
impl CordicConfig {
/// Scan a TrigGraph and build a config with default f for every CORDIC op found.
pub fn scan(graph: &TrigGraph) -> Self {
let mut config = Self::default();
for op in &graph.nodes {
if let Some(cop) = trig_op_to_cordic(op) {
config.ops.entry(cop).or_insert(config.default_f);
}
}
config
}
/// Scan a TrigGraph and build config from a file representation.
/// Overrides are applied in order; last match wins.
pub fn from_file(graph: &TrigGraph, file: &CordicConfigFile) -> Self {
let mut config = Self {
word_bits: file.defaults.word_bits,
default_f: file.defaults.f,
ops: BTreeMap::new(),
};
// Populate with defaults for all ops found in graph
for op in &graph.nodes {
if let Some(cop) = trig_op_to_cordic(op) {
config.ops.entry(cop).or_insert(config.default_f);
}
}
// Apply overrides in order
for ov in &file.overrides {
if let Ok(re) = regex::Regex::new(&ov.op) {
for (&cop, f_val) in config.ops.iter_mut() {
if re.is_match(cop.name()) {
*f_val = ov.f;
}
}
}
}
config
}
/// Parse from TOML string, resolving against the given graph.
pub fn from_toml(graph: &TrigGraph, toml_str: &str) -> Result<Self, toml::de::Error> {
let file: CordicConfigFile = toml::from_str(toml_str)?;
Ok(Self::from_file(graph, &file))
}
/// Serialize the current config to a TOML file representation.
pub fn to_file(&self) -> CordicConfigFile {
// Group ops by f value to produce compact overrides
let mut by_f: BTreeMap<u8, Vec<CordicOp>> = BTreeMap::new();
for (&op, &f) in &self.ops {
if f != self.default_f {
by_f.entry(f).or_default().push(op);
}
}
let overrides = by_f.into_iter().map(|(f, ops)| {
let pattern = ops.iter()
.map(|op| op.name())
.collect::<Vec<_>>()
.join("|");
Override { op: pattern, f }
}).collect();
CordicConfigFile {
defaults: Defaults {
word_bits: self.word_bits,
f: self.default_f,
},
overrides,
}
}
/// Serialize to TOML string.
pub fn to_toml(&self) -> String {
let file = self.to_file();
toml::to_string_pretty(&file).unwrap_or_default()
}
/// Get iteration count for a specific operation.
pub fn iterations_for(&self, op: CordicOp) -> u8 {
self.ops.get(&op).copied().unwrap_or(self.default_f)
}
/// Get the set of unique iteration counts used, for precomputing tables.
pub fn unique_iteration_counts(&self) -> Vec<u8> {
let mut counts: Vec<u8> = self.ops.values().copied().collect();
counts.sort();
counts.dedup();
if counts.is_empty() {
counts.push(self.default_f);
}
counts
}
}
fn trig_op_to_cordic(op: &TrigOp) -> Option<CordicOp> {
match op {
TrigOp::Sin(_) => Some(CordicOp::Sin),
TrigOp::Cos(_) => Some(CordicOp::Cos),
TrigOp::Tan(_) => Some(CordicOp::Tan),
TrigOp::Asin(_) => Some(CordicOp::Asin),
TrigOp::Acos(_) => Some(CordicOp::Acos),
TrigOp::Atan(_) => Some(CordicOp::Atan),
TrigOp::Sinh(_) => Some(CordicOp::Sinh),
TrigOp::Cosh(_) => Some(CordicOp::Cosh),
TrigOp::Tanh(_) => Some(CordicOp::Tanh),
TrigOp::Asinh(_) => Some(CordicOp::Asinh),
TrigOp::Acosh(_) => Some(CordicOp::Acosh),
TrigOp::Atanh(_) => Some(CordicOp::Atanh),
TrigOp::Sqrt(_) => Some(CordicOp::Sqrt),
TrigOp::Exp(_) => Some(CordicOp::Exp),
TrigOp::Ln(_) => Some(CordicOp::Ln),
TrigOp::Hypot(_, _) => Some(CordicOp::Hypot),
TrigOp::Atan2(_, _) => Some(CordicOp::Atan2),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use cord_trig::ir::TrigOp;
#[test]
fn scan_discovers_ops() {
let mut g = TrigGraph::new();
let angle = g.push(TrigOp::Const(1.0));
let s = g.push(TrigOp::Sin(angle));
let c = g.push(TrigOp::Cos(angle));
let _ = g.push(TrigOp::Add(s, c));
g.set_output(3);
let config = CordicConfig::scan(&g);
assert!(config.ops.contains_key(&CordicOp::Sin));
assert!(config.ops.contains_key(&CordicOp::Cos));
assert!(!config.ops.contains_key(&CordicOp::Tan));
assert_eq!(config.ops.len(), 2);
}
#[test]
fn override_applies() {
let mut g = TrigGraph::new();
let a = g.push(TrigOp::Const(1.0));
let _ = g.push(TrigOp::Sin(a));
let _ = g.push(TrigOp::Cos(a));
let _ = g.push(TrigOp::Sinh(a));
g.set_output(1);
let file = CordicConfigFile {
defaults: Defaults { word_bits: 32, f: 32 },
overrides: vec![
Override { op: "sin|cos".into(), f: 24 },
Override { op: "sinh".into(), f: 16 },
],
};
let config = CordicConfig::from_file(&g, &file);
assert_eq!(config.iterations_for(CordicOp::Sin), 24);
assert_eq!(config.iterations_for(CordicOp::Cos), 24);
assert_eq!(config.iterations_for(CordicOp::Sinh), 16);
}
#[test]
fn toml_roundtrip() {
let mut g = TrigGraph::new();
let a = g.push(TrigOp::Const(1.0));
let _ = g.push(TrigOp::Sin(a));
let _ = g.push(TrigOp::Cos(a));
g.set_output(1);
let mut config = CordicConfig::scan(&g);
config.ops.insert(CordicOp::Sin, 24);
let toml_str = config.to_toml();
let config2 = CordicConfig::from_toml(&g, &toml_str).unwrap();
assert_eq!(config2.iterations_for(CordicOp::Sin), 24);
assert_eq!(config2.iterations_for(CordicOp::Cos), 32);
}
}

View File

@ -1,4 +1,6 @@
use cord_trig::{TrigGraph, TrigOp}; use cord_trig::{TrigGraph, TrigOp};
use crate::compiler::{CORDICProgram, CordicTable};
use crate::ops::{atan_table, cordic_gain};
/// CORDIC evaluator: evaluates a TrigGraph using only integer /// CORDIC evaluator: evaluates a TrigGraph using only integer
/// shifts, adds, and comparisons. No floating point trig. /// shifts, adds, and comparisons. No floating point trig.
@ -6,33 +8,32 @@ use cord_trig::{TrigGraph, TrigOp};
/// Proof that the entire pipeline compiles down to /// Proof that the entire pipeline compiles down to
/// binary arithmetic — shift, add, compare, repeat. /// binary arithmetic — shift, add, compare, repeat.
pub struct CORDICEvaluator { pub struct CORDICEvaluator {
word_bits: u8,
frac_bits: u8, frac_bits: u8,
atan_table: Vec<i64>, instr_table_idx: Vec<u8>,
gain: i64, tables: Vec<CordicTable>,
} }
impl CORDICEvaluator { impl CORDICEvaluator {
pub fn new(word_bits: u8) -> Self { pub fn new(word_bits: u8) -> Self {
let frac_bits = word_bits - 1; let frac_bits = word_bits - 1;
let iterations = word_bits; CORDICEvaluator {
frac_bits,
// Precompute atan(2^-i) as fixed-point instr_table_idx: Vec::new(),
let atan_table: Vec<i64> = (0..iterations) tables: vec![CordicTable {
.map(|i| { iterations: word_bits,
let angle = (2.0f64).powi(-(i as i32)).atan(); atan_table: atan_table(word_bits),
(angle * (1i64 << frac_bits) as f64).round() as i64 gain: cordic_gain(word_bits, frac_bits),
}) }],
.collect();
// CORDIC gain K = product of 1/sqrt(1 + 2^{-2i})
let mut k = 1.0f64;
for i in 0..iterations {
k *= 1.0 / (1.0 + (2.0f64).powi(-2 * i as i32)).sqrt();
} }
let gain = (k * (1i64 << frac_bits) as f64).round() as i64; }
CORDICEvaluator { word_bits, frac_bits, atan_table, gain } /// Create an evaluator from a compiled program, inheriting per-op tables.
pub fn from_program(program: &CORDICProgram) -> Self {
CORDICEvaluator {
frac_bits: program.word_bits - 1,
instr_table_idx: program.instr_table_idx.clone(),
tables: program.tables.clone(),
}
} }
/// Convert f64 to fixed-point. /// Convert f64 to fixed-point.
@ -72,71 +73,57 @@ impl CORDICEvaluator {
(((a as i128) << self.frac_bits) / b as i128) as i64 (((a as i128) << self.frac_bits) / b as i128) as i64
} }
/// CORDIC rotation mode: given angle z, compute (cos(z), sin(z)). /// Resolve the table for a given instruction index.
/// Input z is fixed-point radians. fn table_for(&self, instr_idx: usize) -> &CordicTable {
/// Returns (x, y) = (cos(z), sin(z)) in fixed-point. if self.instr_table_idx.is_empty() || self.tables.is_empty() {
/// // Fallback: use legacy single table via a static-lifetime trick
/// Algorithm: // won't work — build a table reference from self fields instead
/// Start with x = K (gain), y = 0, z = angle return &self.tables[0];
/// For each iteration i: }
/// if z >= 0: rotate positive (d = +1) let tidx = self.instr_table_idx.get(instr_idx).copied().unwrap_or(0) as usize;
/// else: rotate negative (d = -1) &self.tables[tidx.min(self.tables.len() - 1)]
/// x_new = x - d * (y >> i) }
/// y_new = y + d * (x >> i)
/// z_new = z - d * atan(2^-i) /// CORDIC rotation mode using a specific table.
fn cordic_rotation(&self, angle: i64) -> (i64, i64) { fn cordic_rotation_with(&self, angle: i64, table: &CordicTable) -> (i64, i64) {
let mut x = self.gain; let mut x = table.gain;
let mut y: i64 = 0; let mut y: i64 = 0;
let mut z = angle; let mut z = angle;
for i in 0..self.word_bits as usize { for i in 0..table.iterations as usize {
let d = if z >= 0 { 1i64 } else { -1 }; let d = if z >= 0 { 1i64 } else { -1 };
let x_new = x - d * (y >> i); let x_new = x - d * (y >> i);
let y_new = y + d * (x >> i); let y_new = y + d * (x >> i);
z -= d * self.atan_table[i]; z -= d * table.atan_table[i];
x = x_new; x = x_new;
y = y_new; y = y_new;
} }
(x, y) // (cos, sin) (x, y)
} }
/// CORDIC vectoring mode: given (x, y), compute magnitude and angle. /// CORDIC vectoring mode using a specific table.
/// Returns (magnitude, angle) in fixed-point. fn cordic_vectoring_with(&self, x_in: i64, y_in: i64, table: &CordicTable) -> (i64, i64) {
///
/// Algorithm:
/// Start with x, y, z = 0
/// For each iteration i:
/// if y < 0: rotate positive (d = +1)
/// else: rotate negative (d = -1)
/// x_new = x - d * (y >> i)
/// y_new = y + d * (x >> i)
/// z_new = z - d * atan(2^-i)
/// Result: x ≈ sqrt(x₀² + y₀²) / K, z ≈ atan2(y₀, x₀)
fn cordic_vectoring(&self, x_in: i64, y_in: i64) -> (i64, i64) {
let mut x = x_in; let mut x = x_in;
let mut y = y_in; let mut y = y_in;
let mut z: i64 = 0; let mut z: i64 = 0;
// Handle negative x by reflecting into right half-plane
let negate_x = x < 0; let negate_x = x < 0;
if negate_x { if negate_x {
x = -x; x = -x;
y = -y; y = -y;
} }
for i in 0..self.word_bits as usize { for i in 0..table.iterations as usize {
let d = if y < 0 { 1i64 } else { -1 }; let d = if y < 0 { 1i64 } else { -1 };
let x_new = x - d * (y >> i); let x_new = x - d * (y >> i);
let y_new = y + d * (x >> i); let y_new = y + d * (x >> i);
z -= d * self.atan_table[i]; z -= d * table.atan_table[i];
x = x_new; x = x_new;
y = y_new; y = y_new;
} }
// Vectoring output: x_final = (1/K) * sqrt(x0^2 + y0^2). let magnitude = self.fixed_mul(x, table.gain);
// self.gain stores K (~0.6073). Multiply to recover true magnitude.
let magnitude = self.fixed_mul(x, self.gain);
if negate_x { if negate_x {
let pi = self.to_fixed(std::f64::consts::PI); let pi = self.to_fixed(std::f64::consts::PI);
@ -147,10 +134,12 @@ impl CORDICEvaluator {
} }
} }
/// Evaluate the entire trig graph using only CORDIC operations. /// Evaluate the entire trig graph using only CORDIC operations.
/// Returns the output as f64 (converted from fixed-point at the end). /// Returns the output as f64 (converted from fixed-point at the end).
pub fn evaluate(&self, graph: &TrigGraph, x: f64, y: f64, z: f64) -> f64 { pub fn evaluate(&self, graph: &TrigGraph, x: f64, y: f64, z: f64) -> f64 {
let mut vals = vec![0i64; graph.nodes.len()]; let mut vals = vec![0i64; graph.nodes.len()];
let use_per_instr = !self.instr_table_idx.is_empty() && self.tables.len() > 1;
for (i, op) in graph.nodes.iter().enumerate() { for (i, op) in graph.nodes.iter().enumerate() {
vals[i] = match op { vals[i] = match op {
@ -167,40 +156,44 @@ impl CORDICEvaluator {
TrigOp::Abs(a) => vals[*a as usize].abs(), TrigOp::Abs(a) => vals[*a as usize].abs(),
TrigOp::Sin(a) => { TrigOp::Sin(a) => {
let (_, sin) = self.cordic_rotation(vals[*a as usize]); let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let (_, sin) = self.cordic_rotation_with(vals[*a as usize], t);
sin sin
} }
TrigOp::Cos(a) => { TrigOp::Cos(a) => {
let (cos, _) = self.cordic_rotation(vals[*a as usize]); let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let (cos, _) = self.cordic_rotation_with(vals[*a as usize], t);
cos cos
} }
TrigOp::Tan(a) => { TrigOp::Tan(a) => {
let (cos, sin) = self.cordic_rotation(vals[*a as usize]); let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let (cos, sin) = self.cordic_rotation_with(vals[*a as usize], t);
self.fixed_div(sin, cos) self.fixed_div(sin, cos)
} }
TrigOp::Asin(a) => { TrigOp::Asin(a) => {
// asin(x) = atan2(x, sqrt(1-x²)) let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let x = vals[*a as usize]; let xv = vals[*a as usize];
let one = self.to_fixed(1.0); let one = self.to_fixed(1.0);
let x2 = self.fixed_mul(x, x); let x2 = self.fixed_mul(xv, xv);
let rem = one - x2; let rem = one - x2;
let sqrt_rem = self.fixed_sqrt(rem); let sqrt_rem = self.fixed_sqrt(rem);
let (_, angle) = self.cordic_vectoring(sqrt_rem, x); let (_, angle) = self.cordic_vectoring_with(sqrt_rem, xv, t);
angle angle
} }
TrigOp::Acos(a) => { TrigOp::Acos(a) => {
// acos(x) = atan2(sqrt(1-x²), x) let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let x = vals[*a as usize]; let xv = vals[*a as usize];
let one = self.to_fixed(1.0); let one = self.to_fixed(1.0);
let x2 = self.fixed_mul(x, x); let x2 = self.fixed_mul(xv, xv);
let rem = one - x2; let rem = one - x2;
let sqrt_rem = self.fixed_sqrt(rem); let sqrt_rem = self.fixed_sqrt(rem);
let (_, angle) = self.cordic_vectoring(x, sqrt_rem); let (_, angle) = self.cordic_vectoring_with(xv, sqrt_rem, t);
angle angle
} }
TrigOp::Atan(a) => { TrigOp::Atan(a) => {
let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let one = self.to_fixed(1.0); let one = self.to_fixed(1.0);
let (_, angle) = self.cordic_vectoring(one, vals[*a as usize]); let (_, angle) = self.cordic_vectoring_with(one, vals[*a as usize], t);
angle angle
} }
TrigOp::Sinh(a) => self.to_fixed(self.to_float(vals[*a as usize]).sinh()), TrigOp::Sinh(a) => self.to_fixed(self.to_float(vals[*a as usize]).sinh()),
@ -214,11 +207,13 @@ impl CORDICEvaluator {
TrigOp::Ln(a) => self.to_fixed(self.to_float(vals[*a as usize]).ln()), TrigOp::Ln(a) => self.to_fixed(self.to_float(vals[*a as usize]).ln()),
TrigOp::Hypot(a, b) => { TrigOp::Hypot(a, b) => {
let (mag, _) = self.cordic_vectoring(vals[*a as usize], vals[*b as usize]); let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let (mag, _) = self.cordic_vectoring_with(vals[*a as usize], vals[*b as usize], t);
mag mag
} }
TrigOp::Atan2(a, b) => { TrigOp::Atan2(a, b) => {
let (_, angle) = self.cordic_vectoring(vals[*b as usize], vals[*a as usize]); let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] };
let (_, angle) = self.cordic_vectoring_with(vals[*b as usize], vals[*a as usize], t);
angle angle
} }
@ -290,4 +285,47 @@ mod tests {
assert!((cordic_val - float_val).abs() < 0.1, assert!((cordic_val - float_val).abs() < 0.1,
"sphere inside: CORDIC={cordic_val}, f64={float_val}"); "sphere inside: CORDIC={cordic_val}, f64={float_val}");
} }
#[test]
fn test_per_op_config_roundtrip() {
use crate::config::{CordicConfig, CordicOp, CordicConfigFile, Defaults, Override};
use crate::compiler::CompileConfig;
let mut g = TrigGraph::new();
let angle = g.push(TrigOp::Const(std::f64::consts::FRAC_PI_4));
let s = g.push(TrigOp::Sin(angle));
let a = g.push(TrigOp::Const(3.0));
let b = g.push(TrigOp::Const(4.0));
let h = g.push(TrigOp::Hypot(a, b));
let _ = g.push(TrigOp::Add(s, h));
g.set_output(5);
// Config: sin at 16 iterations, hypot at 24
let file = CordicConfigFile {
defaults: Defaults { word_bits: 32, f: 32 },
overrides: vec![
Override { op: "sin".into(), f: 16 },
Override { op: "hypot".into(), f: 24 },
],
};
let cfg = CordicConfig::from_file(&g, &file);
assert_eq!(cfg.iterations_for(CordicOp::Sin), 16);
assert_eq!(cfg.iterations_for(CordicOp::Hypot), 24);
// Compile with config
let compile_cfg = CompileConfig {
word_bits: 32,
cordic: Some(cfg),
};
let program = crate::CORDICProgram::compile(&g, &compile_cfg);
assert!(program.tables.len() >= 2);
// Evaluate from program
let eval = CORDICEvaluator::from_program(&program);
let result = eval.evaluate(&g, 0.0, 0.0, 0.0);
// Should still produce a reasonable result (sin(pi/4) + hypot(3,4) ≈ 0.707 + 5 = 5.707)
assert!((result - 5.707).abs() < 0.5,
"per-op config: result={result}, expected ~5.707");
}
} }

View File

@ -8,8 +8,10 @@
//! reference is typically zero at the precision boundary. //! reference is typically zero at the precision boundary.
pub mod compiler; pub mod compiler;
pub mod config;
pub mod ops; pub mod ops;
pub mod eval; pub mod eval;
pub use compiler::CORDICProgram; pub use compiler::CORDICProgram;
pub use config::CordicConfig;
pub use eval::CORDICEvaluator; pub use eval::CORDICEvaluator;

View File

@ -33,6 +33,8 @@ pub struct Layers {
pub trig: bool, pub trig: bool,
pub shader: bool, pub shader: bool,
pub cordic: bool, pub cordic: bool,
#[serde(default)]
pub config: bool,
} }
impl Default for Manifest { impl Default for Manifest {
@ -46,6 +48,7 @@ impl Default for Manifest {
trig: false, trig: false,
shader: false, shader: false,
cordic: false, cordic: false,
config: false,
}, },
} }
} }

View File

@ -66,4 +66,16 @@ impl<R: Read + Seek> ZcdReader<R> {
} }
Ok(None) Ok(None)
} }
/// Read CORDIC configuration (TOML).
pub fn read_config(&mut self) -> Result<Option<String>> {
match self.archive.by_name("config/cordic.toml") {
Ok(mut file) => {
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(Some(buf))
}
Err(_) => Ok(None),
}
}
} }

View File

@ -66,6 +66,15 @@ impl<W: Write + Seek> ZcdWriter<W> {
Ok(()) Ok(())
} }
/// Write CORDIC configuration (TOML).
pub fn write_config(&mut self, config_toml: &str) -> Result<()> {
let options = SimpleFileOptions::default();
self.zip.start_file("config/cordic.toml", options)?;
self.zip.write_all(config_toml.as_bytes())?;
self.manifest.layers.config = true;
Ok(())
}
pub fn finish(mut self) -> Result<W> { pub fn finish(mut self) -> Result<W> {
let manifest_json = serde_json::to_string_pretty(&self.manifest)?; let manifest_json = serde_json::to_string_pretty(&self.manifest)?;
let options = SimpleFileOptions::default(); let options = SimpleFileOptions::default();

View File

@ -168,9 +168,15 @@ fn cmd_build(input: &std::path::Path, output: Option<PathBuf>, word_bits: u8) ->
let (source, graph) = load_trig_graph(input)?; let (source, graph) = load_trig_graph(input)?;
let wgsl = cord_shader::generate_wgsl_from_trig(&graph); let wgsl = cord_shader::generate_wgsl_from_trig(&graph);
let config = cord_cordic::compiler::CompileConfig { word_bits }; // Auto-generate CORDIC config by scanning the graph
let cordic_cfg = cord_cordic::CordicConfig::scan(&graph);
let config = cord_cordic::compiler::CompileConfig {
word_bits,
cordic: Some(cordic_cfg.clone()),
};
let cordic = cord_cordic::CORDICProgram::compile(&graph, &config); let cordic = cord_cordic::CORDICProgram::compile(&graph, &config);
let cordic_bytes = cordic.to_bytes(); let cordic_bytes = cordic.to_bytes();
let config_toml = cordic_cfg.to_toml();
let file = std::fs::File::create(&out_path) let file = std::fs::File::create(&out_path)
.with_context(|| format!("creating {}", out_path.display()))?; .with_context(|| format!("creating {}", out_path.display()))?;
@ -187,6 +193,7 @@ fn cmd_build(input: &std::path::Path, output: Option<PathBuf>, word_bits: u8) ->
} }
writer.write_shader(&wgsl)?; writer.write_shader(&wgsl)?;
writer.write_cordic(&cordic_bytes, word_bits)?; writer.write_cordic(&cordic_bytes, word_bits)?;
writer.write_config(&config_toml)?;
writer.finish()?; writer.finish()?;
println!("wrote {}", out_path.display()); println!("wrote {}", out_path.display());
@ -236,7 +243,11 @@ fn cmd_decompile(
let graph = cord_sdf::sdf_to_trig(&result.sdf); let graph = cord_sdf::sdf_to_trig(&result.sdf);
let wgsl = cord_shader::generate_wgsl_from_trig(&graph); let wgsl = cord_shader::generate_wgsl_from_trig(&graph);
let cordic_config = cord_cordic::compiler::CompileConfig { word_bits }; let cordic_cfg = cord_cordic::CordicConfig::scan(&graph);
let cordic_config = cord_cordic::compiler::CompileConfig {
word_bits,
cordic: Some(cordic_cfg),
};
let cordic = cord_cordic::CORDICProgram::compile(&graph, &cordic_config); let cordic = cord_cordic::CORDICProgram::compile(&graph, &cordic_config);
let cordic_bytes = cordic.to_bytes(); let cordic_bytes = cordic.to_bytes();