Skip to content
8 changes: 8 additions & 0 deletions crates/mohu-dtype/src/dtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,14 @@ impl fmt::Display for DType {
}
}

impl std::str::FromStr for DType {
type Err = MohuError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
DType::from_str(s)
}
}
Comment on lines +526 to +532

// ─── TryFrom ─────────────────────────────────────────────────────────────────

impl TryFrom<&str> for DType {
Expand Down
8 changes: 4 additions & 4 deletions crates/mohu-dtype/src/finfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl FloatInfo {
let tiny = half::f16::MIN_POSITIVE.to_f64();
// Smallest subnormal: 2^{-24}
let smallest_sub = 5.960_464_477_539_063e-8_f64;
let precision = (10_u32 as f64 * f64::log10(2.0)).floor() as u32; // 3
let precision = (10_f64 * f64::log10(2.0)).floor() as u32; // 3
Self {
dtype: DType::F16,
bits: 16,
Expand Down Expand Up @@ -127,7 +127,7 @@ impl FloatInfo {
let max = half::bf16::MAX.to_f64();
let tiny = half::bf16::MIN_POSITIVE.to_f64();
let smallest_sub = 9.183_549_615_799_121e-41_f64;
let precision = (7_u32 as f64 * f64::log10(2.0)).floor() as u32; // 2
let precision = (7_f64 * f64::log10(2.0)).floor() as u32; // 2
Self {
dtype: DType::BF16,
bits: 16,
Expand Down Expand Up @@ -155,7 +155,7 @@ impl FloatInfo {
let max = f32::MAX as f64;
let tiny = f32::MIN_POSITIVE as f64;
let smallest_sub = 1.401_298_464_324_817e-45_f64;
let precision = (23_u32 as f64 * f64::log10(2.0)).floor() as u32; // 6
let precision = (23_f64 * f64::log10(2.0)).floor() as u32; // 6
Self {
dtype: DType::F32,
bits: 32,
Expand Down Expand Up @@ -183,7 +183,7 @@ impl FloatInfo {
let max = f64::MAX;
let tiny = f64::MIN_POSITIVE;
let smallest_sub = 5.0e-324_f64;
let precision = (52_u32 as f64 * f64::log10(2.0)).floor() as u32; // 15
let precision = (52_f64 * f64::log10(2.0)).floor() as u32; // 15
Self {
dtype: DType::F64,
bits: 64,
Expand Down
1 change: 0 additions & 1 deletion crates/mohu-dtype/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
/// | [`dispatch_signed!`] | signed integers + floats |
/// | [`for_each_dtype!`] | invoke a macro for every dtype (codegen helper) |
/// | [`assert_dtype!`] | assert a DType at runtime or return an error |

// ─── dtype_of! ───────────────────────────────────────────────────────────────
Comment on lines 19 to 22

/// Returns the `DType` constant for a Rust primitive type literal.
Expand Down
51 changes: 26 additions & 25 deletions crates/mohu-error/src/reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ impl<'a> ErrorReporter<'a> {
}

fn fmt_full(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let root = ErrorChain::root(self.error);
let code = root.code();
let root = ErrorChain::root(self.error);
let code = root.code();
let depth = ErrorChain::depth(self.error);

// ── header line ───────────────────────────────────────────────────
Expand All @@ -170,17 +170,17 @@ impl<'a> ErrorReporter<'a> {
writeln!(
f,
"{dim} context chain:{reset}",
dim = self.c(ansi::DIM),
dim = self.c(ansi::DIM),
reset = self.reset(),
)?;
for (i, ctx) in ctxs.iter().enumerate().rev() {
writeln!(
f,
" {dim}{arrow}{reset} {ctx}",
dim = self.c(ansi::DIM),
dim = self.c(ansi::DIM),
arrow = if i == 0 { "└─" } else { "├─" },
reset = self.reset(),
ctx = ctx,
ctx = ctx,
)?;
}
}
Expand All @@ -192,9 +192,9 @@ impl<'a> ErrorReporter<'a> {
writeln!(
f,
" {cyan}hint{reset}: {hint}",
cyan = self.c(ansi::BOLD_CYAN),
cyan = self.c(ansi::BOLD_CYAN),
reset = self.reset(),
hint = hint,
hint = hint,
)?;
}
}
Expand All @@ -203,24 +203,25 @@ impl<'a> ErrorReporter<'a> {
writeln!(
f,
" {dim}[{code}] {domain} error{reset}",
dim = self.c(ansi::DIM),
code = code,
dim = self.c(ansi::DIM),
code = code,
domain = code.domain(),
reset = self.reset(),
reset = self.reset(),
)?;

Ok(())
}

fn fmt_json(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let root = ErrorChain::root(self.error);
let code = root.code() as u32;
let kind = crate::kind::ErrorKind::from(root.code());
let root = ErrorChain::root(self.error);
let code = root.code() as u32;
let kind = crate::kind::ErrorKind::from(root.code());
let depth = ErrorChain::depth(self.error);

// Build context array
let ctxs = ErrorChain::context_messages(self.error);
let ctx_json: Vec<String> = ctxs.iter()
let ctxs = ErrorChain::context_messages(self.error);
let ctx_json: Vec<String> = ctxs
.iter()
.map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
.collect();

Expand All @@ -237,12 +238,12 @@ impl<'a> ErrorReporter<'a> {
write!(
f,
r#"{{"code":{code},"kind":"{kind}","message":"{primary}","context":[{ctx}],"hints":[{hints}],"chain_depth":{depth}}}"#,
code = code,
kind = kind,
code = code,
kind = kind,
primary = primary,
ctx = ctx_json.join(","),
hints = hints.join(","),
depth = depth,
ctx = ctx_json.join(","),
hints = hints.join(","),
depth = depth,
)
}
}
Expand All @@ -251,8 +252,8 @@ impl fmt::Display for ErrorReporter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.mode {
ReportMode::Compact => self.fmt_compact(f),
ReportMode::Full => self.fmt_full(f),
ReportMode::Json => self.fmt_json(f),
ReportMode::Full => self.fmt_full(f),
ReportMode::Json => self.fmt_json(f),
}
}
}
Expand Down Expand Up @@ -295,8 +296,8 @@ impl Severity {
pub fn label(self) -> &'static str {
match self {
Self::Warning => "warning",
Self::Error => "error",
Self::Fatal => "fatal",
Self::Error => "error",
Self::Fatal => "fatal",
}
}
}
Expand All @@ -316,7 +317,7 @@ impl MohuError {
pub fn severity(&self) -> Severity {
match ErrorChain::root(self) {
MohuError::Internal(_) => Severity::Fatal,
_ => Severity::Error,
_ => Severity::Error,
}
}
}
19 changes: 5 additions & 14 deletions crates/mohu-error/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/// - [`assert_err_kind!`] — assert the broad error kind
/// - [`assert_shape_err!`] — assert a specific shape mismatch
/// - [`assert_err_chain!`] — assert the depth of the context chain
use crate::{codes::ErrorCode, kind::ErrorKind, MohuError, MohuResult};
use crate::{MohuError, MohuResult, codes::ErrorCode, kind::ErrorKind};

// ─── assertion helpers (non-macro) ───────────────────────────────────────────

Expand All @@ -27,15 +27,10 @@ use crate::{codes::ErrorCode, kind::ErrorKind, MohuError, MohuResult};
/// let err = assert_err(r, "should fail on zero divisor");
/// assert!(matches!(err, MohuError::DivisionByZero));
/// ```
pub fn assert_err<T: std::fmt::Debug>(
result: MohuResult<T>,
context: &str,
) -> MohuError {
pub fn assert_err<T: std::fmt::Debug>(result: MohuResult<T>, context: &str) -> MohuError {
match result {
Err(e) => e,
Ok(v) => panic!(
"assert_err failed ({context}): expected Err(_), got Ok({v:?})"
),
Ok(v) => panic!("assert_err failed ({context}): expected Err(_), got Ok({v:?})"),
}
}

Expand All @@ -45,9 +40,7 @@ pub fn assert_err<T: std::fmt::Debug>(
pub fn assert_ok<T>(result: MohuResult<T>, context: &str) -> T {
match result {
Ok(v) => v,
Err(e) => panic!(
"assert_ok failed ({context}): expected Ok(_), got Err({e})"
),
Err(e) => panic!("assert_ok failed ({context}): expected Ok(_), got Err({e})"),
}
}

Expand Down Expand Up @@ -109,9 +102,7 @@ pub fn assert_shape_err<T: std::fmt::Debug>(
);
}
}
other => panic!(
"assert_shape_err: expected ShapeMismatch, got {other:?}"
),
other => panic!("assert_shape_err: expected ShapeMismatch, got {other:?}"),
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/mohu-fft/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ rayon.workspace = true
rustfft.workspace = true
num-complex.workspace = true
num-traits.workspace = true

[package.metadata.cargo-machete]
ignored = ["mohu-buffer", "mohu-dtype", "num-traits", "rayon"]
78 changes: 77 additions & 1 deletion crates/mohu-fft/src/freq.rs
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
// freq — implementation pending
/// Frequency-axis helpers similar to NumPy's `fftfreq` and `fftshift`.

/// Return the Discrete Fourier Transform sample frequencies for a window of
/// length `n` and sample spacing `d` (default 1.0).
pub fn fftfreq(n: usize, d: f64) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
let val = 1.0 / (n as f64 * d);
let mut freqs = Vec::with_capacity(n);
// For even `n` the Nyquist frequency (n/2) should be negative (-0.5/d).
// Use `pos_len = (n + 1) / 2` as the number of non-negative frequency bins.
let pos_len = (n + 1) / 2;
for i in 0..pos_len {
freqs.push(i as f64 * val);
}
for i in pos_len..n {
freqs.push(-((n - i) as f64) * val);
}
freqs
}

/// Alias for `fftfreq` for real-input transforms; behavior is identical.
pub fn rfftfreq(n: usize, d: f64) -> Vec<f64> {
Comment on lines +23 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix rfftfreq doc wording; it is not identical to fftfreq.

The implementation returns only non-negative bins (n/2 + 1), so the current “behavior is identical” statement is misleading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/mohu-fft/src/freq.rs` around lines 23 - 24, The docstring for rfftfreq
is inaccurate: update the comment for the pub fn rfftfreq(n: usize, d: f64) ->
Vec<f64> to state that it returns only the non-negative frequency bins used for
real-input (length = n/2 + 1) and that it differs from fftfreq by omitting
negative frequencies (i.e., it follows rfft conventions), and include the
formula/units for d if present so callers understand the bin spacing.

if n == 0 {
return Vec::new();
}
let val = 1.0 / (n as f64 * d);
let count = n / 2 + 1;
(0..count).map(|i| i as f64 * val).collect()
}

/// Shift the zero-frequency component to the center of the spectrum.
pub fn fftshift<T: Clone>(mut v: Vec<T>) -> Vec<T> {
let n = v.len();
if n == 0 {
return v;
}
let mid = n / 2;
let mut out = Vec::with_capacity(n);
out.extend_from_slice(&v[mid..]);
out.extend_from_slice(&v[..mid]);
out
}

/// The inverse of `fftshift`.
pub fn ifftshift<T: Clone>(mut v: Vec<T>) -> Vec<T> {
let n = v.len();
if n == 0 {
return v;
}
let mid = (n + 1) / 2;
let mut out = Vec::with_capacity(n);
out.extend_from_slice(&v[mid..]);
out.extend_from_slice(&v[..mid]);
out
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_fftfreq_len() {
let f = fftfreq(4, 1.0);
assert_eq!(f, vec![0.0, 0.25, -0.5, -0.25]);
}

#[test]
fn test_fftshift() {
let v = vec![0, 1, 2, 3];
let s = fftshift(v.clone());
assert_eq!(s, vec![2, 3, 0, 1]);
let r = ifftshift(s);
assert_eq!(r, v);
}
}
78 changes: 77 additions & 1 deletion crates/mohu-fft/src/transform.rs
Original file line number Diff line number Diff line change
@@ -1 +1,77 @@
// transform — implementation pending
use num_complex::Complex;
use rustfft::{FftPlanner, num_complex::Complex as RComplex};

use crate::Norm;

/// Compute the 1-D FFT of `input` with optional length `n` and `norm` mode.
/// If `n` is larger than `input.len()` the input is zero-padded; if smaller,
/// it is truncated.
pub fn fft(input: &[Complex<f64>], n: Option<usize>, norm: Norm) -> Vec<Complex<f64>> {
let len = n.unwrap_or(input.len());
if len == 0 {
return Vec::new();
}
let mut buf: Vec<RComplex<f64>> = vec![RComplex::new(0.0, 0.0); len];
for (i, v) in input.iter().take(len).enumerate() {
buf[i] = RComplex::new(v.re, v.im);
}

let mut planner = FftPlanner::new();
let fft = planner.plan_fft_forward(len);
fft.process(&mut buf);

// apply forward normalization
let scale = match norm {
Norm::Backward => 1.0,
Norm::Ortho => 1.0 / (len as f64).sqrt(),
Norm::Forward => 1.0 / (len as f64),
};

buf.into_iter()
.map(|c| Complex::new(c.re * scale, c.im * scale))
.collect()
}

/// Compute the 1-D inverse FFT (IFFT) of `input` with optional length `n` and `norm` mode.
pub fn ifft(input: &[Complex<f64>], n: Option<usize>, norm: Norm) -> Vec<Complex<f64>> {
let len = n.unwrap_or(input.len());
if len == 0 {
return Vec::new();
}
let mut buf: Vec<RComplex<f64>> = vec![RComplex::new(0.0, 0.0); len];
for (i, v) in input.iter().take(len).enumerate() {
buf[i] = RComplex::new(v.re, v.im);
}

let mut planner = FftPlanner::new();
let ifft = planner.plan_fft_inverse(len);
ifft.process(&mut buf);

// apply backward normalization
let scale = match norm {
Norm::Backward => 1.0 / (len as f64),
Norm::Ortho => 1.0 / (len as f64).sqrt(),
Norm::Forward => 1.0,
};

buf.into_iter()
.map(|c| Complex::new(c.re * scale, c.im * scale))
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
use num_complex::Complex;

#[test]
fn roundtrip_fft_ifft() {
let input: Vec<Complex<f64>> = (0..8).map(|i| Complex::new(i as f64, 0.0)).collect();
let out = fft(&input, None, Norm::Backward);
let back = ifft(&out, None, Norm::Backward);
for (a, b) in input.iter().zip(back.iter()) {
assert!((a.re - b.re).abs() < 1e-9);
assert!((a.im - b.im).abs() < 1e-9);
}
}
}
Loading