From 5eed41557fb91e685c6e99e7a0a23eed3d230053 Mon Sep 17 00:00:00 2001 From: jess Date: Tue, 31 Mar 2026 13:30:19 -0700 Subject: [PATCH] Per-operation CORDIC config system with ZCD archive integration --- crates/cord-cordic/Cargo.toml | 3 + crates/cord-cordic/src/compiler.rs | 116 ++++++++++- crates/cord-cordic/src/config.rs | 305 +++++++++++++++++++++++++++++ crates/cord-cordic/src/eval.rs | 176 ++++++++++------- crates/cord-cordic/src/lib.rs | 2 + crates/cord-format/src/lib.rs | 3 + crates/cord-format/src/read.rs | 12 ++ crates/cord-format/src/write.rs | 9 + src/main.rs | 15 +- 9 files changed, 564 insertions(+), 77 deletions(-) create mode 100644 crates/cord-cordic/src/config.rs diff --git a/crates/cord-cordic/Cargo.toml b/crates/cord-cordic/Cargo.toml index f6aba83..e56c49c 100644 --- a/crates/cord-cordic/Cargo.toml +++ b/crates/cord-cordic/Cargo.toml @@ -10,3 +10,6 @@ categories = ["mathematics", "no-std"] [dependencies] cord-trig = { path = "../cord-trig" } +serde = { version = "1", features = ["derive"] } +toml = "0.8" +regex = "1" diff --git a/crates/cord-cordic/src/compiler.rs b/crates/cord-cordic/src/compiler.rs index c4f502f..4e1bfb9 100644 --- a/crates/cord-cordic/src/compiler.rs +++ b/crates/cord-cordic/src/compiler.rs @@ -1,4 +1,6 @@ +use std::collections::BTreeMap; use cord_trig::{TrigGraph, TrigOp}; +use crate::config::{CordicConfig, CordicOp}; use crate::ops::*; /// 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. /// The instruction list parallels the TrigGraph node list — compilation /// 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)] pub struct CORDICProgram { pub word_bits: u8, pub instructions: Vec, pub output: u32, + /// One entry per unique iteration count. + pub tables: Vec, + /// 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, + /// Legacy fields for backward compat with single-table programs. + pub atan_table: Vec, + 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, pub gain: i64, } @@ -18,11 +38,12 @@ pub struct CORDICProgram { #[derive(Debug, Clone)] pub struct CompileConfig { pub word_bits: u8, + pub cordic: Option, } impl Default for CompileConfig { 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. /// /// Each TrigOp node becomes one CORDICInstr. Constants are converted - /// from f64 to fixed-point at compile time. Everything else is a - /// direct structural mapping. + /// from f64 to fixed-point at compile time. Per-operation iteration + /// 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 { let frac_bits = config.word_bits - 1; let to_fixed = |val: f64| -> 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 = 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 = 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 { TrigOp::InputX => CORDICInstr::InputX, TrigOp::InputY => CORDICInstr::InputY, @@ -77,12 +163,21 @@ impl CORDICProgram { } }).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 { word_bits: config.word_bits, instructions, output: graph.output, - atan_table: atan_table(config.word_bits), - gain: cordic_gain(config.word_bits, frac_bits), + tables, + instr_table_idx, + atan_table: primary.atan_table, + gain: primary.gain, } } @@ -153,6 +248,15 @@ impl CORDICProgram { 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, + }) } } diff --git a/crates/cord-cordic/src/config.rs b/crates/cord-cordic/src/config.rs new file mode 100644 index 0000000..1788d91 --- /dev/null +++ b/crates/cord-cordic/src/config.rs @@ -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::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, +} + +#[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, +} + +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 { + 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> = 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::>() + .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 { + let mut counts: Vec = 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 { + 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); + } +} diff --git a/crates/cord-cordic/src/eval.rs b/crates/cord-cordic/src/eval.rs index 33d5ebd..1b4372f 100644 --- a/crates/cord-cordic/src/eval.rs +++ b/crates/cord-cordic/src/eval.rs @@ -1,4 +1,6 @@ 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 /// 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 /// binary arithmetic — shift, add, compare, repeat. pub struct CORDICEvaluator { - word_bits: u8, frac_bits: u8, - atan_table: Vec, - gain: i64, + instr_table_idx: Vec, + tables: Vec, } impl CORDICEvaluator { pub fn new(word_bits: u8) -> Self { let frac_bits = word_bits - 1; - let iterations = word_bits; - - // Precompute atan(2^-i) as fixed-point - let atan_table: Vec = (0..iterations) - .map(|i| { - let angle = (2.0f64).powi(-(i as i32)).atan(); - (angle * (1i64 << frac_bits) as f64).round() as i64 - }) - .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(); + CORDICEvaluator { + frac_bits, + instr_table_idx: Vec::new(), + tables: vec![CordicTable { + iterations: word_bits, + atan_table: atan_table(word_bits), + gain: cordic_gain(word_bits, frac_bits), + }], } - 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. @@ -72,71 +73,57 @@ impl CORDICEvaluator { (((a as i128) << self.frac_bits) / b as i128) as i64 } - /// CORDIC rotation mode: given angle z, compute (cos(z), sin(z)). - /// Input z is fixed-point radians. - /// Returns (x, y) = (cos(z), sin(z)) in fixed-point. - /// - /// Algorithm: - /// Start with x = K (gain), y = 0, z = angle - /// For each iteration i: - /// if z >= 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) - fn cordic_rotation(&self, angle: i64) -> (i64, i64) { - let mut x = self.gain; + /// Resolve the table for a given instruction index. + fn table_for(&self, instr_idx: usize) -> &CordicTable { + if self.instr_table_idx.is_empty() || self.tables.is_empty() { + // Fallback: use legacy single table via a static-lifetime trick + // won't work — build a table reference from self fields instead + return &self.tables[0]; + } + let tidx = self.instr_table_idx.get(instr_idx).copied().unwrap_or(0) as usize; + &self.tables[tidx.min(self.tables.len() - 1)] + } + + /// CORDIC rotation mode using a specific table. + fn cordic_rotation_with(&self, angle: i64, table: &CordicTable) -> (i64, i64) { + let mut x = table.gain; let mut y: i64 = 0; 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 x_new = x - d * (y >> i); let y_new = y + d * (x >> i); - z -= d * self.atan_table[i]; + z -= d * table.atan_table[i]; x = x_new; y = y_new; } - (x, y) // (cos, sin) + (x, y) } - /// CORDIC vectoring mode: given (x, y), compute magnitude and angle. - /// Returns (magnitude, angle) in fixed-point. - /// - /// 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) { + /// CORDIC vectoring mode using a specific table. + fn cordic_vectoring_with(&self, x_in: i64, y_in: i64, table: &CordicTable) -> (i64, i64) { let mut x = x_in; let mut y = y_in; let mut z: i64 = 0; - // Handle negative x by reflecting into right half-plane let negate_x = x < 0; if negate_x { x = -x; 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 x_new = x - d * (y >> i); let y_new = y + d * (x >> i); - z -= d * self.atan_table[i]; + z -= d * table.atan_table[i]; x = x_new; y = y_new; } - // Vectoring output: x_final = (1/K) * sqrt(x0^2 + y0^2). - // self.gain stores K (~0.6073). Multiply to recover true magnitude. - let magnitude = self.fixed_mul(x, self.gain); + let magnitude = self.fixed_mul(x, table.gain); if negate_x { let pi = self.to_fixed(std::f64::consts::PI); @@ -147,10 +134,12 @@ impl CORDICEvaluator { } } + /// Evaluate the entire trig graph using only CORDIC operations. /// 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 { 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() { vals[i] = match op { @@ -167,40 +156,44 @@ impl CORDICEvaluator { TrigOp::Abs(a) => vals[*a as usize].abs(), 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 } 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 } 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) } TrigOp::Asin(a) => { - // asin(x) = atan2(x, sqrt(1-x²)) - let x = vals[*a as usize]; + let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] }; + let xv = vals[*a as usize]; 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 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 } TrigOp::Acos(a) => { - // acos(x) = atan2(sqrt(1-x²), x) - let x = vals[*a as usize]; + let t = if use_per_instr { self.table_for(i) } else { &self.tables[0] }; + let xv = vals[*a as usize]; 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 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 } 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 (_, angle) = self.cordic_vectoring(one, vals[*a as usize]); + let (_, angle) = self.cordic_vectoring_with(one, vals[*a as usize], t); angle } 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::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 } 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 } @@ -290,4 +285,47 @@ mod tests { assert!((cordic_val - float_val).abs() < 0.1, "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"); + } } diff --git a/crates/cord-cordic/src/lib.rs b/crates/cord-cordic/src/lib.rs index b269a49..351f70d 100644 --- a/crates/cord-cordic/src/lib.rs +++ b/crates/cord-cordic/src/lib.rs @@ -8,8 +8,10 @@ //! reference is typically zero at the precision boundary. pub mod compiler; +pub mod config; pub mod ops; pub mod eval; pub use compiler::CORDICProgram; +pub use config::CordicConfig; pub use eval::CORDICEvaluator; diff --git a/crates/cord-format/src/lib.rs b/crates/cord-format/src/lib.rs index f369b0f..155be30 100644 --- a/crates/cord-format/src/lib.rs +++ b/crates/cord-format/src/lib.rs @@ -33,6 +33,8 @@ pub struct Layers { pub trig: bool, pub shader: bool, pub cordic: bool, + #[serde(default)] + pub config: bool, } impl Default for Manifest { @@ -46,6 +48,7 @@ impl Default for Manifest { trig: false, shader: false, cordic: false, + config: false, }, } } diff --git a/crates/cord-format/src/read.rs b/crates/cord-format/src/read.rs index 95a98ac..a4dc13c 100644 --- a/crates/cord-format/src/read.rs +++ b/crates/cord-format/src/read.rs @@ -66,4 +66,16 @@ impl ZcdReader { } Ok(None) } + + /// Read CORDIC configuration (TOML). + pub fn read_config(&mut self) -> Result> { + 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), + } + } } diff --git a/crates/cord-format/src/write.rs b/crates/cord-format/src/write.rs index 83053cc..867e22a 100644 --- a/crates/cord-format/src/write.rs +++ b/crates/cord-format/src/write.rs @@ -66,6 +66,15 @@ impl ZcdWriter { 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 { let manifest_json = serde_json::to_string_pretty(&self.manifest)?; let options = SimpleFileOptions::default(); diff --git a/src/main.rs b/src/main.rs index 55f2654..b03ca33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,9 +168,15 @@ fn cmd_build(input: &std::path::Path, output: Option, word_bits: u8) -> let (source, graph) = load_trig_graph(input)?; 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_bytes = cordic.to_bytes(); + let config_toml = cordic_cfg.to_toml(); let file = std::fs::File::create(&out_path) .with_context(|| format!("creating {}", out_path.display()))?; @@ -187,6 +193,7 @@ fn cmd_build(input: &std::path::Path, output: Option, word_bits: u8) -> } writer.write_shader(&wgsl)?; writer.write_cordic(&cordic_bytes, word_bits)?; + writer.write_config(&config_toml)?; writer.finish()?; println!("wrote {}", out_path.display()); @@ -236,7 +243,11 @@ fn cmd_decompile( let graph = cord_sdf::sdf_to_trig(&result.sdf); 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_bytes = cordic.to_bytes();