merge integration
This commit is contained in:
commit
3ce7aa2fdf
|
|
@ -10,3 +10,6 @@ categories = ["mathematics", "no-std"]
|
|||
|
||||
[dependencies]
|
||||
cord-trig = { path = "../cord-trig" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
regex = "1"
|
||||
|
|
|
|||
|
|
@ -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<CORDICInstr>,
|
||||
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 gain: i64,
|
||||
}
|
||||
|
|
@ -18,11 +38,12 @@ pub struct CORDICProgram {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct CompileConfig {
|
||||
pub word_bits: u8,
|
||||
pub cordic: Option<CordicConfig>,
|
||||
}
|
||||
|
||||
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<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 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<i64>,
|
||||
gain: i64,
|
||||
instr_table_idx: Vec<u8>,
|
||||
tables: Vec<CordicTable>,
|
||||
}
|
||||
|
||||
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<i64> = (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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,4 +66,16 @@ impl<R: Read + Seek> ZcdReader<R> {
|
|||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,15 @@ impl<W: Write + Seek> ZcdWriter<W> {
|
|||
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> {
|
||||
let manifest_json = serde_json::to_string_pretty(&self.manifest)?;
|
||||
let options = SimpleFileOptions::default();
|
||||
|
|
|
|||
15
src/main.rs
15
src/main.rs
|
|
@ -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 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<PathBuf>, 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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue