Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dbf32f7
Add error enums
SpookyYomo Mar 11, 2025
66d829a
Add non-allocating variants of Error enum variants
SpookyYomo Mar 11, 2025
42c2903
Add ndarray_conv to core
SpookyYomo Apr 25, 2025
54e75e4
Add Result "alias" to core
SpookyYomo Apr 25, 2025
2d1f079
Add error messages
SpookyYomo Apr 25, 2025
333c992
Add error messages arising from ndarray_conv
SpookyYomo Apr 26, 2025
bfcbc37
Add linear convolution through ndarray_conv in num-rs space
SpookyYomo Apr 27, 2025
ea43d53
Change np-convolve to use ArrayView instead of ArrayBase in the 1st arg
SpookyYomo Apr 28, 2025
aee705d
Remove kernel vs signal debug assertion
SpookyYomo Apr 28, 2025
1b484a0
Add sci-rs-core Cargo.toml
SpookyYomo Apr 29, 2025
5fbca6b
Merge remote-tracking branch 'origin/core' into np-convolve
SpookyYomo Apr 29, 2025
fc25134
Build sci-rs-core with features used to build sci-rs
SpookyYomo Apr 29, 2025
6ff0282
Change lfilter to return an error if axis argument is invalid
SpookyYomo Jun 11, 2025
391cdc7
Remove unnecessary reborrow
SpookyYomo Jun 12, 2025
90b0bd4
Assert that lfilter arguments are not 0-dimensional arrays
SpookyYomo Jun 13, 2025
f21a6bc
Update ndarray-conv to 0.5.0 in core
SpookyYomo Jul 6, 2025
8d1ae35
Merge branch 'core' into np-convolve
SpookyYomo Jul 7, 2025
182658d
Fix linear convolve behaviour
SpookyYomo Jul 14, 2025
54fc05f
Add initial lfilter implementation for 1d
SpookyYomo Apr 30, 2025
c6734de
Remove unnecessary cfg parameter
SpookyYomo Jul 15, 2025
fc31076
Add more lfilter tests in the context of 1-dimensional input without zi
SpookyYomo Jul 15, 2025
f5e6579
Refactor Axis yielding logic
SpookyYomo Jul 16, 2025
a6c22f7
Include linear_filter function
SpookyYomo Jul 17, 2025
c6d4504
Improve internal function name and documentation
SpookyYomo Jul 18, 2025
a33bf92
Partially undo refactor to optimize `zi.is_none()` path
SpookyYomo Jul 23, 2025
b8954e5
Propagate error from convolve instead of unwrap within lfilter
SpookyYomo Jul 24, 2025
f0c971a
Introduce LFilter as trait instead
SpookyYomo Jul 25, 2025
301b779
Move lfilter implementation into macro
SpookyYomo Jul 28, 2025
8d99c3f
Add happy zi.is_some() case
SpookyYomo Jul 30, 2025
47be4c4
Implement path that handles the unexpected shape of zi
SpookyYomo Aug 11, 2025
f7d2c68
Touch up lfilter documentation
SpookyYomo Aug 12, 2025
c2cd592
Change lfilter_zi documentation to point to new lfilter implementation
SpookyYomo Aug 13, 2025
396887a
Remove unnecessary heap allocation of zi
SpookyYomo Aug 15, 2025
4aaa73a
Fix up docstring in lfilter
SpookyYomo Sep 23, 2025
279cc09
Prepare for IxDyn compatible lfilter function
SpookyYomo Oct 2, 2025
207c625
Add IxDyn compatible lfilter
SpookyYomo Oct 3, 2025
8fb1e52
Remove useless conversion of Vec<Ix> in lfilter
SpookyYomo Oct 4, 2025
ae13a03
Provision types for lfilter return types
SpookyYomo Oct 5, 2025
a3b6b54
Change LFilter trait bounds
SpookyYomo Oct 18, 2025
4cd1042
Move `lfilter::ndarray_shape_...` to `arraytools::..._st`
SpookyYomo Oct 18, 2025
ab84226
Change zeroing to direct heap-reservation for lfilter intermediates
SpookyYomo Oct 24, 2025
ef26b6c
Add const inlining of user's `axis: Option<isize>` to `usize`
SpookyYomo Oct 24, 2025
952223d
Change lfilter(for IxDyn arrays) to use get_axis_dyn
SpookyYomo Oct 25, 2025
61d4392
Change check_and_get_axis_* to return only `usize`
SpookyYomo Oct 26, 2025
07ba98a
Move `lfilter::check_and_get_axis*` to `arraytools::`
SpookyYomo Oct 27, 2025
d100c40
Remove unnecessary trait requirements of LFilter
SpookyYomo Oct 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions sci-rs-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "sci-rs-core"
version = "0.0.0"
edition = "2021"
authors = ["Jacob Trueb <[email protected]>"]
description = "Core library for sci-rs internals."
license = "MIT OR Apache-2.0"
repository = "https://github.com/qsib-cbie/sci-rs.git"
homepage = "https://github.com/qsib-cbie/sci-rs.git"
readme = "../README.md"
keywords = ["scipy", "dsp", "signal", "filter", "design"]
categories = ["science", "mathematics", "no-std", "embedded"]


[package.metadata.docs.rs]
all-features = true

[features]
default = ['alloc']

# Allow allocating vecs, matrices, etc.
alloc = []

# Enable FFT and standard library features
std = ['alloc']

[dependencies]
ndarray = { version = "0.16.1", default-features = false }
ndarray-conv = { version = "0.5.0" }
num-traits = { version = "0.2.15", default-features = false }
79 changes: 79 additions & 0 deletions sci-rs-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! Core library for sci-rs.

#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::format;

use core::{error, fmt};

pub type Result<T> = core::result::Result<T, Error>;

/// Errors raised whilst running sci-rs.
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
/// Argument parsed into function were invalid.
#[cfg(feature = "alloc")]
InvalidArg {
/// The invalid arg
arg: alloc::string::String,
/// Explaining why arg is invalid.
reason: alloc::string::String,
},
/// Argument parsed into function were invalid.
#[cfg(not(feature = "alloc"))]
InvalidArg,
/// Two or more optional arguments passed into functions conflict.
#[cfg(feature = "alloc")]
ConflictArg {
/// Explaining what arg is invalid.
reason: alloc::string::String,
},
/// Two or more optional arguments passed into functions conflict.
#[cfg(not(feature = "alloc"))]
ConflictArg,
/// Errors raised by [ndarray_conv::Error]
#[cfg(feature = "alloc")]
Conv { reason: alloc::string::String },
/// Errors raised by [ndarray_conv::Error]
#[cfg(not(feature = "alloc"))]
Conv,
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
#[cfg(feature = "alloc")]
Error::InvalidArg { arg, reason } =>
format!("Invalid Argument on arg = {} with reason = {}", arg, reason),
#[cfg(not(feature = "alloc"))]
Error::InvalidArg =>
"There were invalid arguments. Reasons not shown without `alloc` feature.",
#[cfg(feature = "alloc")]
Error::ConflictArg { reason } =>
format!("Conflicting Arguments with reason = {}", reason),
#[cfg(not(feature = "alloc"))]
Error::ConflictArg =>
"There were conflicting arguments. Reasons not shown without `alloc` feature.",
#[cfg(feature = "alloc")]
Error::Conv { reason } => format!(
"An error occurred during the convolution from ndarray_conv with reason {}.",
reason
),
#[cfg(not(feature = "alloc"))]
Error::Conv => "An error occurred during the convolution from ndarray_conv. Reasons not shown without `alloc` feature.",
}
)
}
}

impl error::Error for Error {}

/// Collection of numpy-like functions for use by sci-rs.
/// Provide behaviour parity against Numpy, even if the types are not identical.
pub mod num_rs;
135 changes: 135 additions & 0 deletions sci-rs-core/src/num_rs/convolve/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
mod ndarray_conv_binds;

use crate::{Error, Result};
use alloc::string::ToString;
use ndarray::{Array1, ArrayView1};
use ndarray_conv::{ConvExt, PaddingMode};

/// Convolution mode determines behavior near edges and output size
pub enum ConvolveMode {
/// Full convolution, output size is `in1.len() + in2.len() - 1`
Full,
/// Valid convolution, output size is `max(in1.len(), in2.len()) - min(in1.len(), in2.len()) + 1`
Valid,
/// Same convolution, output size is `in1.len()`
Same,
}

/// Best effort parallel behaviour with numpy's convolve method. We take `v` as the convolution
/// kernel.
///
/// Returns the discrete, linear convolution of two one-dimensional sequences.
///
/// # Parameters
/// * `a` : (N,) [[array_like]]([ndarray::Array1])
/// Signal to be (linearly) convolved.
/// * `v` : (M,) [[array_like]]([ndarray::Array1])
/// Second one-dimensional input array.
/// * `mode` : [ConvolveMode]
/// [ConvolveMode::Full]:
/// By default, mode is 'full'. This returns the convolution at each point of overlap, with an
/// output shape of (N+M-1,). At the end-points of the convolution, the signals do not overlap
/// completely, and boundary effects may be seen.
///
/// [ConvolveMode::Same]:
/// Mode 'same' returns output of length ``max(M, N)``. Boundary effects are still visible.
///
/// [ConvolveMode::Valid]:
/// Mode 'valid' returns output of length ``max(M, N) - min(M, N) + 1``. The convolution
/// product is only given for points where the signals overlap completely. Values outside the
/// signal boundary have no effect.
///
/// # Panics
/// We assume that `v` is shorter than `a`.
///
/// # Examples
/// With [ConvolveMode::Full]:
/// ```
/// use ndarray::array;
/// use sci_rs_core::num_rs::{ConvolveMode, convolve};
///
/// let a = array![1., 2., 3.];
/// let v = array![0., 1., 0.5];
///
/// let expected = array![0., 1., 2.5, 4., 1.5];
/// let result = convolve((&a).into(), (&v).into(), ConvolveMode::Full).unwrap();
/// assert_eq!(result, expected);
/// ```
/// With [ConvolveMode::Same]:
/// ```
/// use ndarray::array;
/// use sci_rs_core::num_rs::{ConvolveMode, convolve};
///
/// let a = array![1., 2., 3.];
/// let v = array![0., 1., 0.5];
///
/// let expected = array![1., 2.5, 4.];
/// let result = convolve((&a).into(), (&v).into(), ConvolveMode::Same).unwrap();
/// assert_eq!(result, expected);
/// ```
/// With [ConvolveMode::Same]:
/// ```
/// use ndarray::array;
/// use sci_rs_core::num_rs::{ConvolveMode, convolve};
///
/// let a = array![1., 2., 3.];
/// let v = array![0., 1., 0.5];
///
/// let expected = array![2.5];
/// let result = convolve((&a).into(), (&v).into(), ConvolveMode::Valid).unwrap();
/// assert_eq!(result, expected);
/// ```
pub fn convolve<T>(a: ArrayView1<T>, v: ArrayView1<T>, mode: ConvolveMode) -> Result<Array1<T>>
where
T: num_traits::NumAssign + core::marker::Copy,
{
// Convolve
let result = a.conv(&v, mode.into(), PaddingMode::Zeros);
#[cfg(feature = "alloc")]
{
result.map_err(|e| Error::Conv {
reason: e.to_string(),
})
}
#[cfg(not(feature = "alloc"))]
{
result.map_err({ Error::Conv })
}
}

#[cfg(test)]
mod linear_convolve {
use super::*;
use alloc::vec;
use ndarray::array;

#[test]
fn full() {
let a = array![1., 2., 3.];
let v = array![0., 1., 0.5];

let expected = array![0., 1., 2.5, 4., 1.5];
let result = convolve((&a).into(), (&v).into(), ConvolveMode::Full).unwrap();
assert_eq!(result, expected);
}

#[test]
fn same() {
let a = array![1., 2., 3.];
let v = array![0., 1., 0.5];

let expected = array![1., 2.5, 4.];
let result = convolve((&a).into(), (&v).into(), ConvolveMode::Same).unwrap();
assert_eq!(result, expected);
}

#[test]
fn valid() {
let a = array![1., 2., 3.];
let v = array![0., 1., 0.5];

let expected = array![2.5];
let result = convolve((&a).into(), (&v).into(), ConvolveMode::Valid).unwrap();
assert_eq!(result, expected);
}
}
12 changes: 12 additions & 0 deletions sci-rs-core/src/num_rs/convolve/ndarray_conv_binds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use super::ConvolveMode;
use ndarray_conv::ConvMode;

impl<const N: usize> From<ConvolveMode> for ConvMode<N> {
fn from(value: ConvolveMode) -> Self {
match value {
ConvolveMode::Full => ConvMode::Full,
ConvolveMode::Same => ConvMode::Same,
ConvolveMode::Valid => ConvMode::Valid,
}
}
}
4 changes: 4 additions & 0 deletions sci-rs-core/src/num_rs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#[cfg(feature = "alloc")]
mod convolve;
#[cfg(feature = "alloc")]
pub use convolve::*;
5 changes: 3 additions & 2 deletions sci-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ all-features = true
default = ['alloc']

# Allow allocating vecs, matrices, etc.
alloc = ['nalgebra/alloc', 'nalgebra/libm', 'kalmanfilt/alloc']
alloc = ['nalgebra/alloc', 'nalgebra/libm', 'kalmanfilt/alloc', 'sci-rs-core/alloc']

# Enable FFT and standard library features
std = ['nalgebra/std', 'nalgebra/macros', 'rustfft', 'alloc']
std = ['nalgebra/std', 'nalgebra/macros', 'rustfft', 'alloc','sci-rs-core/std']

# Enable debug plotting through python system calls
plot = ['std']
Expand All @@ -36,6 +36,7 @@ lstsq = { version = "0.6.0", default-features = false }
rustfft = { version = "6.2.0", optional = true }
kalmanfilt = { version = "0.3.0", default-features = false }
gaussfilt = { version = "0.1.3", default-features = false }
sci-rs-core = { path = "../sci-rs-core", default-features = false }

[dev-dependencies]
approx = "0.5.1"
Expand Down
10 changes: 1 addition & 9 deletions sci-rs/src/signal/convolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,7 @@ use nalgebra::Complex;
use num_traits::{Float, FromPrimitive, Signed, Zero};
use rustfft::{FftNum, FftPlanner};

/// Convolution mode determines behavior near edges and output size
pub enum ConvolveMode {
/// Full convolution, output size is `in1.len() + in2.len() - 1`
Full,
/// Valid convolution, output size is `max(in1.len(), in2.len()) - min(in1.len(), in2.len()) + 1`
Valid,
/// Same convolution, output size is `in1.len()`
Same,
}
pub use sci_rs_core::num_rs::ConvolveMode;

/// Performs FFT-based convolution on two slices of floating point values.
///
Expand Down
Loading
Loading