├── .github
├── codecov.yml
├── dependabot.yml
└── workflows
│ ├── check.yml
│ ├── scheduled.yml
│ └── test.yml
├── .gitignore
├── Cargo.toml
├── LICENSE
├── README.md
├── ROADMAP.md
├── build.rs
├── src
├── lib.rs
├── odr
│ ├── mod.rs
│ └── polynomial.rs
├── optimize
│ ├── criteria.rs
│ ├── least_square
│ │ └── mod.rs
│ ├── metric.rs
│ ├── min_scalar
│ │ ├── golden.rs
│ │ └── mod.rs
│ ├── mod.rs
│ ├── root_scalar
│ │ ├── bracket.rs
│ │ ├── fixed_point.rs
│ │ ├── halley.rs
│ │ ├── mod.rs
│ │ ├── newton.rs
│ │ ├── polynomial.rs
│ │ └── secant.rs
│ └── util.rs
├── signal
│ ├── band_filter.rs
│ ├── convolution
│ │ └── mod.rs
│ ├── error.rs
│ ├── filter_design
│ │ ├── bessel.rs
│ │ ├── butter.rs
│ │ ├── butterord.rs
│ │ ├── cheby1.rs
│ │ ├── cheby2.rs
│ │ ├── ellip.rs
│ │ ├── error.rs
│ │ └── mod.rs
│ ├── fir_filter_design
│ │ ├── firwin1.rs
│ │ ├── mod.rs
│ │ ├── pass_zero.rs
│ │ ├── tools.rs
│ │ └── windows.rs
│ ├── mod.rs
│ ├── output_type
│ │ ├── ba.rs
│ │ ├── mod.rs
│ │ ├── sos.rs
│ │ └── zpk.rs
│ ├── signal_tools.rs
│ └── tools
│ │ └── mod.rs
├── special
│ ├── kv.rs
│ ├── mod.rs
│ └── trig.rs
└── tools
│ ├── complex.rs
│ └── mod.rs
└── tests
├── optimize
├── least_square.rs
├── main.rs
├── min_scaler.rs
├── polynomial.rs
└── root_scalar.rs
├── signal
├── bessel.rs
├── butter.rs
├── cheby1.rs
├── cheby2.rs
├── common.rs
├── fir_filter_design.rs
├── fir_filter_design_windows.rs
├── lp2bf_zpk.rs
├── main.rs
└── signal_tools.rs
└── special.rs
/.github/codecov.yml:
--------------------------------------------------------------------------------
1 | # ref: https://docs.codecov.com/docs/codecovyml-reference
2 | coverage:
3 | # Hold ourselves to a high bar
4 | range: 85..100
5 | round: down
6 | precision: 1
7 | status:
8 | # ref: https://docs.codecov.com/docs/commit-status
9 | project:
10 | default:
11 | # Avoid false negatives
12 | threshold: 1%
13 |
14 | # Test files aren't important for coverage
15 | ignore:
16 | - "tests"
17 |
18 | # Make comments less noisy
19 | comment:
20 | layout: "files"
21 | require_changes: yes
22 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: cargo
8 | directory: /
9 | schedule:
10 | interval: daily
11 | ignore:
12 | - dependency-name: "*"
13 | # patch and minor updates don't matter for libraries
14 | # remove this ignore rule if your package has binaries
15 | update-types:
16 | - "version-update:semver-patch"
17 | - "version-update:semver-minor"
18 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | permissions:
2 | contents: read
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 | name: check
12 | jobs:
13 | doc:
14 | name: Documentation
15 | runs-on: ubuntu-latest
16 | env:
17 | RUSTDOCFLAGS: -Dwarnings
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: dtolnay/rust-toolchain@nightly
21 | - uses: dtolnay/install@cargo-docs-rs
22 | - name: install fortran compiler
23 | run: sudo apt install -y gfortran
24 | - run: cargo docs-rs
25 | fmt:
26 | runs-on: ubuntu-latest
27 | name: stable / fmt
28 | steps:
29 | - uses: actions/checkout@v4
30 | with:
31 | submodules: true
32 | - name: Install stable
33 | uses: dtolnay/rust-toolchain@stable
34 | with:
35 | components: rustfmt
36 | - name: install fortran compiler
37 | run: sudo apt install -y gfortran
38 | - name: cargo fmt --check
39 | run: cargo fmt --check
40 | clippy:
41 | runs-on: ubuntu-latest
42 | name: ${{ matrix.toolchain }} / clippy
43 | permissions:
44 | contents: read
45 | checks: write
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | toolchain: [stable, beta]
50 | steps:
51 | - uses: actions/checkout@v4
52 | with:
53 | submodules: true
54 | - name: Install ${{ matrix.toolchain }}
55 | uses: dtolnay/rust-toolchain@master
56 | with:
57 | toolchain: ${{ matrix.toolchain }}
58 | components: clippy
59 | - name: install fortran compiler
60 | run: sudo apt install -y gfortran
61 | - name: cargo clippy
62 | uses: actions-rs/clippy-check@v1
63 | with:
64 | token: ${{ secrets.GITHUB_TOKEN }}
65 | hack:
66 | runs-on: ubuntu-latest
67 | name: ubuntu / stable / features
68 | steps:
69 | - uses: actions/checkout@v4
70 | with:
71 | submodules: true
72 | - name: Install stable
73 | uses: dtolnay/rust-toolchain@stable
74 | - name: install fortran compiler
75 | run: sudo apt install -y gfortran
76 | - name: cargo install cargo-hack
77 | uses: taiki-e/install-action@cargo-hack
78 | # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4
79 | - name: cargo hack
80 | run: cargo hack --feature-powerset check
81 | msrv:
82 | runs-on: ubuntu-latest
83 | # we use a matrix here just because env can't be used in job names
84 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability
85 | strategy:
86 | matrix:
87 | msrv: ["1.64.0"] # 2021 edition requires 1.56
88 | name: ubuntu / ${{ matrix.msrv }}
89 | steps:
90 | - uses: actions/checkout@v4
91 | with:
92 | submodules: true
93 | - name: Install ${{ matrix.msrv }}
94 | uses: dtolnay/rust-toolchain@master
95 | with:
96 | toolchain: ${{ matrix.msrv }}
97 | - name: install fortran compiler
98 | run: sudo apt install -y gfortran
99 | - name: cargo +${{ matrix.msrv }} check
100 | run: cargo check
101 |
--------------------------------------------------------------------------------
/.github/workflows/scheduled.yml:
--------------------------------------------------------------------------------
1 | permissions:
2 | contents: read
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | schedule:
8 | - cron: '7 7 * * *'
9 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5
10 | concurrency:
11 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
12 | cancel-in-progress: true
13 | name: rolling
14 | jobs:
15 | # https://twitter.com/mycoliza/status/1571295690063753218
16 | nightly:
17 | runs-on: ubuntu-latest
18 | name: ubuntu / nightly
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | submodules: true
23 | - name: Install nightly
24 | uses: dtolnay/rust-toolchain@nightly
25 | - name: install fortran compiler
26 | run: sudo apt install -y gfortran
27 | - name: Install python modules
28 | run: pip3 install scipy pandas numpy
29 | - name: cargo generate-lockfile
30 | if: hashFiles('Cargo.lock') == ''
31 | run: cargo generate-lockfile
32 | - name: cargo test --locked
33 | run: cargo test --locked --all-features --all-targets
34 | # https://twitter.com/alcuadrado/status/1571291687837732873
35 | update:
36 | runs-on: ubuntu-latest
37 | name: ubuntu / beta / updated
38 | # There's no point running this if no Cargo.lock was checked in in the
39 | # first place, since we'd just redo what happened in the regular test job.
40 | # Unfortunately, hashFiles only works in if on steps, so we reepeat it.
41 | # if: hashFiles('Cargo.lock') != ''
42 | steps:
43 | - uses: actions/checkout@v4
44 | with:
45 | submodules: true
46 | - name: Install beta
47 | if: hashFiles('Cargo.lock') != ''
48 | uses: dtolnay/rust-toolchain@beta
49 | - name: install fortran compiler
50 | run: sudo apt install -y gfortran
51 | - name: Install python modules
52 | run: pip3 install scipy pandas numpy
53 | - name: cargo update
54 | if: hashFiles('Cargo.lock') != ''
55 | run: cargo update
56 | - name: cargo test
57 | if: hashFiles('Cargo.lock') != ''
58 | run: cargo test --locked --all-features --all-targets
59 | env:
60 | RUSTFLAGS: -D deprecated
61 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | permissions:
2 | contents: read
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | # Spend CI time only on latest ref: https://github.com/jonhoo/rust-ci-conf/pull/5
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
10 | cancel-in-progress: true
11 | name: test
12 | jobs:
13 | required:
14 | runs-on: ubuntu-latest
15 | name: ubuntu / ${{ matrix.toolchain }}
16 | strategy:
17 | matrix:
18 | toolchain: [stable, beta]
19 | steps:
20 | - uses: actions/checkout@v4
21 | with:
22 | submodules: true
23 | - name: Install ${{ matrix.toolchain }}
24 | uses: dtolnay/rust-toolchain@master
25 | with:
26 | toolchain: ${{ matrix.toolchain }}
27 | - name: install fortran compiler
28 | run: sudo apt install -y gfortran
29 | - name: Install python modules
30 | run: pip3 install scipy pandas numpy
31 | - name: cargo generate-lockfile
32 | if: hashFiles('Cargo.lock') == ''
33 | run: cargo generate-lockfile
34 | # https://twitter.com/jonhoo/status/1571290371124260865
35 | - name: cargo test --locked
36 | run: cargo test --locked --all-features --all-targets
37 | # https://github.com/rust-lang/cargo/issues/6669
38 | - name: cargo test --doc
39 | run: cargo test --locked --all-features --doc
40 | coverage:
41 | runs-on: ubuntu-latest
42 | name: ubuntu / stable / coverage
43 | steps:
44 | - uses: actions/checkout@v4
45 | with:
46 | submodules: true
47 | - name: Install stable
48 | uses: dtolnay/rust-toolchain@stable
49 | with:
50 | components: llvm-tools-preview
51 | - name: cargo install cargo-llvm-cov
52 | uses: taiki-e/install-action@cargo-llvm-cov
53 | - name: cargo generate-lockfile
54 | if: hashFiles('Cargo.lock') == ''
55 | run: cargo generate-lockfile
56 | - name: install fortran compiler
57 | run: sudo apt install -y gfortran
58 | - name: Install python modules
59 | run: pip3 install scipy pandas numpy
60 | - name: cargo llvm-cov
61 | run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info
62 | - name: Upload to codecov.io
63 | uses: codecov/codecov-action@v3
64 | with:
65 | fail_ci_if_error: true
66 | env:
67 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 | /complex_bessel
4 |
5 | **/.DS_Store
6 | README.tpl
7 | publish_version.sh
8 |
9 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "sciport-rs"
3 | version = "0.0.3"
4 | edition = "2021"
5 | description = "Rust port of scipy"
6 | authors = ["Christian Belloni"]
7 | readme = "README.md"
8 | license = "MIT"
9 | keywords = ["math", "science", "filter", "dsp"]
10 | categories = ["mathematics", "science", "algorithms"]
11 | homepage = "https://github.com/ChristianBelloni/sciport-rs"
12 | repository = "https://github.com/ChristianBelloni/sciport-rs"
13 |
14 | [dependencies]
15 | num = "0.4"
16 | complex-bessel-rs = { version = "1.2.0" }
17 | ndarray = { version = "0.15.6", features = ["rayon"] }
18 | itertools = "0.13.0"
19 | nalgebra = "0.32.3"
20 | approx = { version = "0.5", features = ["num-complex"] }
21 | thiserror = { version = "1.0" }
22 | #blas-src = { version = "0.9.0", default-features = false, features = ["accelerate"], optional = true }
23 |
24 | [dev-dependencies]
25 | pyo3 = { version = "0.21.2", features = ["full", "auto-initialize"] }
26 | numpy = "0.21.0"
27 | ndarray = { version = "0.15.6", features = ["rayon", "approx-0_5"] }
28 | ndarray-rand = "0.14"
29 | rand = "0.8.5"
30 | lazy_static = "1.4.0"
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Christian Belloni
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sciport-rs
2 |
3 | ## Sciport-rs
4 |
5 | Sciport is a collection of mathematical algorithms ported from the popular python package Scipy
6 |
7 | ## Api design
8 |
9 | The main philosophy behind sciport is to change the api surface of scipy to better utilize the
10 | rich rust typesystem, when deciding between keeping the original function signature and
11 | rewriting it to better represent the valid input space, more often than not we'll decide to
12 | change it.
13 | for example this is the scipy butter filter api:
14 |
15 | ```python
16 | scipy.signal.butter(N: int, Wn: array_like, btype: String, analog: bool, output: String, fs:
17 | float)
18 | ```
19 |
20 | Wn represents a single or a pair of frequencies and btype is the type of filter,
21 | however, a single frequency makes sense only for a subset of btypes and so does a pair,
22 | in our implementation we rewrite this function like:
23 |
24 | ```rust
25 | fn filter(order: u32, band_filter: BandFilter, analog: Sampling) { }
26 | ```
27 |
28 | where T represents the output representation of the filter (Zpk, Ba, Sos), band_filter
29 | encapsulates the original Wn and btype like this:
30 |
31 | ```rust
32 |
33 | pub enum BandFilter {
34 | Highpass(f64),
35 | Lowpass(f64),
36 | Bandpass { low: f64, high: f64 },
37 | Bandstop { low: f64, high: f64 },
38 | }
39 | ```
40 |
41 | and Sampling encapsulates analog and fs (since a sampling rate makes sense only when talking
42 | about a digital filter) like this:
43 |
44 | ```rust
45 | pub enum Sampling {
46 | Analog,
47 | Digital {
48 | fs: f64
49 | }
50 | }
51 | ```
52 |
53 | ## Modules
54 |
55 | ### Signal Processing
56 |
57 | The signal processing toolbox currently contains some filtering functions, a limited set of filter design tools, and a few B-spline interpolation algorithms for 1- and 2-D data. While the B-spline algorithms could technically be placed under the interpolation category, they are included here because they only work with equally-spaced data and make heavy use of filter-theory and transfer-function formalism to provide a fast B-spline transform.
58 |
59 | ### Special
60 |
61 | The main feature of this module is the definition of numerous special functions
62 | of mathematical physics. Available functions include airy, elliptic, bessel, gamma, beta,
63 | hypergeometric, parabolic cylinder, mathieu, spheroidal wave, struve, and kelvin.
64 |
65 |
66 | License: MIT
67 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # Scipy features:
2 |
3 | ## scipy.signal
4 |
5 | ### Convolution
6 | - [ ] convolve
7 | - [ ] correlate
8 | - [ ] fftconvolve
9 | - [ ] oaconvolve
10 | - [ ] convolve2d
11 | - [ ] correlate2d
12 | - [ ] sepfir2d
13 | - [ ] choose_conv_method
14 | - [ ] correlation_lags
15 |
16 | ### B-splines
17 | - [ ] gauss_spline
18 | - [ ] cspline1d
19 | - [ ] qspline1d
20 | - [ ] cspline2d
21 | - [ ] qspline2d
22 | - [ ] cspline1d_eval
23 | - [ ] qspline1d_eval
24 | - [ ] spline_filter
25 |
26 | ### Filtering
27 | TODO!
28 |
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | pub fn main() {
2 | #[cfg(all(target_os = "macos", feature = "blas"))]
3 | println!("cargo:rustc-link-lib=framework=Accelerate");
4 | #[cfg(target_os = "macos")]
5 | println!(
6 | "cargo:rustc-link-arg=-Wl,-rpath,/Library/Developer/CommandLineTools/Library/Frameworks"
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![warn(clippy::all, clippy::nursery)]
2 |
3 | //! # Sciport-rs
4 | //!
5 | //! Sciport is a collection of mathematical algorithms ported from the popular python package Scipy
6 | //!
7 | //! # Api design
8 | //!
9 | //! The main philosophy behind sciport is to change the api surface of scipy to better utilize the
10 | //! rich rust typesystem, when deciding between keeping the original function signature and
11 | //! rewriting it to better represent the valid input space, more often than not we'll decide to
12 | //! change it.
13 | //! for example this is the scipy butter filter api:
14 | //!
15 | //! ```python
16 | //! scipy.signal.butter(N: int, Wn: array_like, btype: String, analog: bool, output: String, fs:
17 | //! float)
18 | //! ```
19 | //!
20 | //! Wn represents a single or a pair of frequencies and btype is the type of filter,
21 | //! however, a single frequency makes sense only for a subset of btypes and so does a pair,
22 | //! in our implementation we rewrite this function like:
23 | //!
24 | //! ```
25 | //! # use sciport_rs::signal::Sampling;
26 | //! # use sciport_rs::signal::band_filter::BandFilter;
27 | //! fn filter(order: u32, band_filter: BandFilter, analog: Sampling) { }
28 | //! ```
29 | //!
30 | //! where T represents the output representation of the filter (Zpk, Ba, Sos), band_filter
31 | //! encapsulates the original Wn and btype like this:
32 | //!
33 | //! ```
34 | //!
35 | //! pub enum BandFilter {
36 | //! Highpass(f64),
37 | //! Lowpass(f64),
38 | //! Bandpass { low: f64, high: f64 },
39 | //! Bandstop { low: f64, high: f64 },
40 | //! }
41 | //! ```
42 | //!
43 | //! and Sampling encapsulates analog and fs (since a sampling rate makes sense only when talking
44 | //! about a digital filter) like this:
45 | //!
46 | //! ```
47 | //! pub enum Sampling {
48 | //! Analog,
49 | //! Digital {
50 | //! fs: f64
51 | //! }
52 | //! }
53 | //! ```
54 | //!
55 | //! # Modules
56 | //!
57 | //! ## Signal Processing
58 | //!
59 | //! The signal processing toolbox currently contains some filtering functions, a limited set of filter design tools, and a few B-spline interpolation algorithms for 1- and 2-D data. While the B-spline algorithms could technically be placed under the interpolation category, they are included here because they only work with equally-spaced data and make heavy use of filter-theory and transfer-function formalism to provide a fast B-spline transform.
60 | //!
61 | //! ## Special
62 | //!
63 | //! The main feature of this module is the definition of numerous special functions
64 | //! of mathematical physics. Available functions include airy, elliptic, bessel, gamma, beta,
65 | //! hypergeometric, parabolic cylinder, mathieu, spheroidal wave, struve, and kelvin.
66 | //!
67 | #[allow(unused)]
68 | pub mod odr;
69 | #[allow(unused)]
70 | pub mod optimize;
71 | pub mod signal;
72 | pub mod special;
73 |
74 | pub(crate) mod tools;
75 |
--------------------------------------------------------------------------------
/src/odr/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod polynomial;
2 |
--------------------------------------------------------------------------------
/src/odr/polynomial.rs:
--------------------------------------------------------------------------------
1 | use itertools::{EitherOrBoth, Itertools};
2 | use nalgebra::{ComplexField, Scalar};
3 | use num::complex::ComplexFloat;
4 | use num::Float;
5 | use std::fmt::{Debug, Display};
6 | use std::ops::{Add, Div, Index, Mul, Sub};
7 | use std::rc::Rc;
8 |
9 | use crate::optimize::least_square;
10 | use crate::optimize::root_scalar::polynomial::{polynomial_roots, IntoComplex};
11 | use crate::optimize::util::Espilon;
12 | use crate::optimize::{IntoMetric, Metric};
13 |
14 | /// # `PolynomialCoef`
15 | /// a common trait for polynomial coefficient, just for display purposes
16 | pub trait PolynomialCoef: ComplexFloat + Clone {
17 | fn coef_to_string(&self) -> String;
18 | }
19 |
20 | /// # `Polynomial`
21 | /// a polynomial struct that represented by a `Vec` of coefficient,
22 | /// which can be `f32`,`f64`,`Complex32`, `Complex64`
23 | ///
24 | /// where the i-th coefficient represent the i-th power's coefficient of the polynomial
25 | #[derive(Debug, Clone)]
26 | pub struct Polynomial
27 | where
28 | T: PolynomialCoef,
29 | {
30 | coef: Vec,
31 | }
32 |
33 | impl Polynomial
34 | where
35 | T: PolynomialCoef,
36 | {
37 | /// while the highest power of the polynomial is zero,
38 | /// pop that coefficient of the polynomial
39 | ///
40 | /// if the polynomial ended up to have no coefficient,
41 | /// push a zero to represent a zero constant
42 | pub fn saturate(mut self) -> Self {
43 | while let Some(&c) = self.coef.last() {
44 | if c == T::zero() {
45 | self.coef.pop();
46 | } else {
47 | break;
48 | }
49 | }
50 | if self.degree() == 0 {
51 | self.coef.push(T::zero());
52 | }
53 | self
54 | }
55 | /// return the degree of the polynomial,
56 | /// aka the highest power the polynomial consisted of
57 | pub fn degree(&self) -> usize {
58 | self.coef.len().clamp(1, usize::MAX) - 1
59 | }
60 | /// iterate its coefficient, from 0-th power.
61 | pub fn iter(&self) -> std::slice::Iter<'_, T> {
62 | self.coef.iter()
63 | }
64 | /// construct a `Polynomial` for a `Vec`
65 | pub fn from_vec(coef: Vec) -> Self {
66 | Self { coef }.saturate()
67 | }
68 | /// return new polynomial with only a zero constant
69 | pub fn zero() -> Self {
70 | Self::from_vec(vec![T::zero()])
71 | }
72 | /// return new polynomial with only a one constant
73 | pub fn one() -> Self {
74 | Self::from_vec(vec![T::one()])
75 | }
76 | /// evaluate the polynomial at `x`
77 | pub fn eval(&self, x: T) -> T {
78 | self.iter().rev().fold(T::zero(), |acc, &c| acc * x + c)
79 | }
80 | /// evaluate the polynomial at `x` for `x` in `xs`
81 | pub fn eval_iter(&self, xs: impl IntoIterator- ) -> Vec {
82 | xs.into_iter().map(|x| self.eval(x)).collect()
83 | }
84 | /// return the multiply of polynomial by `x^p`
85 | pub fn mul_power(&self, p: usize) -> Self {
86 | vec![T::zero(); p].iter().chain(self.iter()).collect()
87 | }
88 | /// return differentiated polynomial
89 | pub fn differentiate(&self) -> Self {
90 | self.iter()
91 | .enumerate()
92 | .filter_map(|(i, &c)| {
93 | if i == 0 {
94 | None
95 | } else {
96 | Some(T::from(i as i64).unwrap() * c)
97 | }
98 | })
99 | .collect()
100 | }
101 | /// construct a new polynomial from roots and multiply constant `k`
102 | pub fn from_roots_k(roots: impl IntoIterator
- , k: T) -> Self {
103 | roots
104 | .into_iter()
105 | .map(|r| Self::from(vec![-r, T::one()]))
106 | .fold(Self::one(), |acc, p| acc * p)
107 | * k
108 | }
109 | /// return the deflated polynomial using horner's method
110 | ///
111 | /// it return the quotient polynomial and the remainder scalar
112 | ///
113 | ///
114 | pub fn deflate(&self, x: T) -> Option<(Self, T)> {
115 | let result = self
116 | .iter()
117 | .rev()
118 | .scan(T::zero(), |carry, &coef| {
119 | let new_coef = coef + *carry;
120 | *carry = new_coef * x;
121 | Some(new_coef)
122 | })
123 | .collect::>();
124 | let (remainder, quotient) = result.split_last()?;
125 | Some((quotient.iter().rev().collect(), remainder.to_owned()))
126 | }
127 |
128 | /// find all root of the polynomial,
129 | ///
130 | /// where all its root will be in complex number data structure
131 | /// i.e. `Complex32` or `Complexf64`
132 | #[must_use]
133 | pub fn roots(&self) -> Vec
134 | where
135 | T: PolynomialCoef + Espilon + IntoMetric + IntoComplex,
136 | C: PolynomialCoef + From + Espilon + IntoMetric + IntoComplex,
137 | M: Metric,
138 | {
139 | polynomial_roots(self)
140 | }
141 | /// calculate the polynomial least square curve fit on data `x` and `y`
142 | /// see `sciport_rs::optimize::least_square::poly_fit`
143 | pub fn poly_fit<'a, Q>(
144 | x: impl IntoIterator
- ,
145 | y: impl IntoIterator
- ,
146 | order: usize,
147 | ) -> Result
148 | where
149 | T: Debug + Display + ComplexField + PolynomialCoef + Espilon,
150 | Q: ComplexFloat + Scalar + Debug + Espilon,
151 | {
152 | least_square::poly_fit(x, y, order)
153 | }
154 | }
155 |
156 | impl Polynomial
157 | where
158 | T: PolynomialCoef,
159 | {
160 | /// take ownership and package the polynomial into `RcT>`
161 | #[must_use]
162 | pub fn as_rc(self) -> Rc T> {
163 | Rc::new(move |x| self.eval(x))
164 | }
165 |
166 | pub fn as_fn(self) -> impl Fn(T) -> T {
167 | move |x| self.eval(x)
168 | }
169 | }
170 |
171 | impl Index for Polynomial
172 | where
173 | T: PolynomialCoef,
174 | {
175 | type Output = T;
176 | fn index(&self, index: usize) -> &Self::Output {
177 | &self.coef[index]
178 | }
179 | }
180 |
181 | impl Mul for Polynomial
182 | where
183 | T: PolynomialCoef,
184 | {
185 | type Output = Self;
186 | fn mul(self, rhs: T) -> Self::Output {
187 | self.iter().map(|&c| c * rhs).collect()
188 | }
189 | }
190 |
191 | #[allow(clippy::suspicious_arithmetic_impl)]
192 | impl Div for Polynomial
193 | where
194 | T: PolynomialCoef,
195 | {
196 | type Output = Self;
197 | fn div(self, rhs: T) -> Self::Output {
198 | self * rhs.recip()
199 | }
200 | }
201 |
202 | impl Add for Polynomial
203 | where
204 | T: PolynomialCoef,
205 | {
206 | type Output = Self;
207 | fn add(self, rhs: Self) -> Self::Output {
208 | self.iter()
209 | .zip_longest(rhs.iter())
210 | .map(|pair| match pair {
211 | EitherOrBoth::Both(&a, &b) => a + b,
212 | EitherOrBoth::Left(&a) => a,
213 | EitherOrBoth::Right(&b) => b,
214 | })
215 | .collect()
216 | }
217 | }
218 |
219 | impl Mul for Polynomial
220 | where
221 | T: PolynomialCoef,
222 | {
223 | type Output = Self;
224 | fn mul(self, rhs: Self) -> Self::Output {
225 | self.iter()
226 | .enumerate()
227 | .map(|(i, &c)| rhs.mul_power(i) * c)
228 | .fold(Self::zero(), |acc, p| acc + p)
229 | }
230 | }
231 |
232 | impl Sub for Polynomial
233 | where
234 | T: PolynomialCoef,
235 | {
236 | type Output = Self;
237 | fn sub(self, rhs: Self) -> Self::Output {
238 | self + rhs * (T::zero() - T::one())
239 | }
240 | }
241 |
242 | impl Display for Polynomial
243 | where
244 | T: PolynomialCoef,
245 | {
246 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 | write!(
248 | f,
249 | "{}",
250 | self.iter()
251 | .enumerate()
252 | .map(|(i, &c)| {
253 | format!(
254 | "{}{}",
255 | c.coef_to_string(),
256 | if i == 0 {
257 | String::new()
258 | } else {
259 | format!(" * x^{i:<3}")
260 | }
261 | )
262 | })
263 | .collect::>()
264 | .join(" + ")
265 | )
266 | }
267 | }
268 |
269 | impl FromIterator for Polynomial
270 | where
271 | T: PolynomialCoef,
272 | {
273 | fn from_iter>(iter: I) -> Self {
274 | Self {
275 | coef: Vec::from_iter(iter),
276 | }
277 | .saturate()
278 | }
279 | }
280 |
281 | impl<'a, T> FromIterator<&'a T> for Polynomial
282 | where
283 | T: PolynomialCoef + 'a,
284 | {
285 | fn from_iter>(iter: I) -> Self {
286 | iter.into_iter().copied().collect()
287 | }
288 | }
289 |
290 | impl From<&[T]> for Polynomial
291 | where
292 | T: PolynomialCoef,
293 | {
294 | fn from(value: &[T]) -> Self {
295 | value.iter().collect()
296 | }
297 | }
298 |
299 | impl From> for Polynomial
300 | where
301 | T: PolynomialCoef,
302 | {
303 | fn from(value: Vec) -> Self {
304 | Self::from_vec(value)
305 | }
306 | }
307 |
308 | impl IntoIterator for Polynomial
309 | where
310 | T: PolynomialCoef,
311 | {
312 | type Item = T;
313 | type IntoIter = std::vec::IntoIter;
314 | fn into_iter(self) -> Self::IntoIter {
315 | self.coef.into_iter()
316 | }
317 | }
318 |
319 | impl PolynomialCoef for T
320 | where
321 | T: ComplexFloat + Clone,
322 | {
323 | fn coef_to_string(&self) -> String {
324 | "coeff doesn't implement display".into()
325 | }
326 | }
327 |
328 | // impl PolynomialCoef for Complex32 {
329 | // fn coef_to_string(&self) -> String {
330 | // format!("({:9.2} + {:9.2}i)", self.re, self.im)
331 | // }
332 | // }
333 | // impl PolynomialCoef for Complex64 {
334 | // fn coef_to_string(&self) -> String {
335 | // format!("({:9.2} + {:9.2}i)", self.re, self.im)
336 | // }
337 | // }
338 |
339 | // impl PolynomialCoef for Complex
340 | // where
341 | // Complex: ComplexFloat + Clone + Debug + 'static,
342 | // T: std::fmt::Display,
343 | // {
344 | // fn coef_to_string(&self) -> String {
345 | // format!("({:9.2} + {:9.2}i)", self.re, self.im)
346 | // }
347 | // }
348 |
--------------------------------------------------------------------------------
/src/optimize/criteria.rs:
--------------------------------------------------------------------------------
1 | use crate::optimize::*;
2 |
3 | /// Criteria
4 | #[derive(Debug, Clone)]
5 | pub struct OptimizeCriteria
6 | where
7 | X: IntoMetric,
8 | F: IntoMetric,
9 | M: Metric,
10 | {
11 | /// Satisfies xatol if `|x-x'| < xatol`
12 | pub xatol: Option,
13 | /// Satisfies xrtol if `|x-x'| < xrtol * x'`
14 | pub xrtol: Option,
15 | /// Satisfies fatol if `|f-f'| < fatol`
16 | pub fatol: Option,
17 | /// Satisfies frtol if `|f-f'| < frtol * f'`
18 | pub frtol: Option,
19 | /// Satisfies fltol if `|f-target_f| < fatol`
20 | pub fltol: Option,
21 | /// Fail if `iter > maxiter`
22 | pub maxiter: Option,
23 | /// specify the metric evaluation type for x
24 | pub x_metric_type: MetricType,
25 | /// specify the metric evaluation type for f
26 | pub f_metric_type: MetricType,
27 | }
28 |
29 | /// Default `xatol`
30 | const DEFAULT_XATOL: f64 = 1e-9;
31 | /// Default `xrtol`
32 | const DEFAULT_XRTOL: f64 = 1e-100;
33 | /// Default `fatol`
34 | const DEFAULT_FATOL: f64 = 1e-9;
35 | /// Default `frtol`
36 | const DEFAULT_FRTOL: f64 = 1e-100;
37 | /// Default `fltol`
38 | const DEFAULT_FLTOL: f64 = 1e-9;
39 | /// Default `maxiter`
40 | const DEFAULT_MAXITER: u64 = 1000;
41 |
42 | impl OptimizeCriteria
43 | where
44 | X: IntoMetric,
45 | F: IntoMetric,
46 | M: Metric,
47 | {
48 | /// Builder Pattern for setting `xatol`
49 | pub fn set_xatol(mut self, tol: Option) -> Self {
50 | self.xatol = tol;
51 | self
52 | }
53 | /// Builder Pattern for setting `xrtol`
54 | pub fn set_xrtol(mut self, tol: Option) -> Self {
55 | self.xrtol = tol;
56 | self
57 | }
58 | /// Builder Pattern for setting `fatol`
59 | pub fn set_fatol(mut self, tol: Option) -> Self {
60 | self.fatol = tol;
61 | self
62 | }
63 | /// Builder Pattern for setting `frtol`
64 | pub fn set_frtol(mut self, tol: Option) -> Self {
65 | self.frtol = tol;
66 | self
67 | }
68 | /// Builder Pattern for setting `fltol`
69 | pub fn set_fltol(mut self, tol: Option) -> Self {
70 | self.fltol = tol;
71 | self
72 | }
73 | /// Builder Pattern for setting `maxiter`
74 | pub fn set_maxiter(mut self, max: Option) -> Self {
75 | self.maxiter = max;
76 | self
77 | }
78 | /// Builder Pattern for setting `x_metric_type`
79 | pub fn set_x_metric_type(mut self, metric_type: MetricType) -> Self {
80 | self.x_metric_type = metric_type;
81 | self
82 | }
83 | /// Builder Pattern for setting `f_metric_type`
84 | pub fn set_f_metric_type(mut self, metric_type: MetricType) -> Self {
85 | self.f_metric_type = metric_type;
86 | self
87 | }
88 |
89 | /// Create a new criteria with no parameter, and default `Metric::L2Norm` for both x and f
90 | pub fn empty() -> Self {
91 | Self {
92 | xatol: None,
93 | xrtol: None,
94 | fatol: None,
95 | frtol: None,
96 | fltol: None,
97 | maxiter: None,
98 | x_metric_type: MetricType::L2Norm,
99 | f_metric_type: MetricType::L2Norm,
100 | }
101 | }
102 | }
103 |
104 | impl Default for OptimizeCriteria
105 | where
106 | X: IntoMetric,
107 | F: IntoMetric,
108 | M: Metric,
109 | {
110 | fn default() -> Self {
111 | OptimizeCriteria {
112 | xatol: M::from(DEFAULT_XATOL),
113 | xrtol: M::from(DEFAULT_XRTOL),
114 | fatol: M::from(DEFAULT_FATOL),
115 | frtol: M::from(DEFAULT_FRTOL),
116 | fltol: M::from(DEFAULT_FLTOL),
117 | maxiter: Some(DEFAULT_MAXITER),
118 | x_metric_type: MetricType::L2Norm,
119 | f_metric_type: MetricType::L2Norm,
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/optimize/least_square/mod.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{Debug, Display};
2 |
3 | use crate::odr::polynomial::{Polynomial, PolynomialCoef};
4 | use crate::optimize::util::Espilon;
5 | use nalgebra::{ComplexField, Scalar};
6 | use num::complex::ComplexFloat;
7 |
8 | /// # poly_fit
9 | /// calculate the polynomial least square curve fit on data `x` and `y`
10 | ///
11 | /// `order` define the order of the returned polynomial.
12 | ///
13 | /// - if `order < x.len()`, the result will be least square curve fitting
14 | ///
15 | /// - if `order = x.len()`, the result will be exact polynomial solve
16 | ///
17 | /// - if `order > x.len()`, the result will be least sqaure of the coefficient of polynomial
18 | ///
19 | /// ## Example
20 | /// ```
21 | /// # use sciport_rs::optimize::least_square::poly_fit;
22 | ///
23 | /// let x = vec![1.0,2.0,3.0];
24 | /// let y = vec![2.0,1.0,2.0];
25 | ///
26 | /// let poly = poly_fit(&x,&y,2).unwrap();
27 | /// ```
28 | ///
29 | /// ## Errors
30 | /// This function will return an error
31 | /// - if the lenght if `x` and `y` are not the same.
32 | /// - the svd solve fail.
33 | pub fn poly_fit<'a, T, Q>(
34 | x: impl IntoIterator
- ,
35 | y: impl IntoIterator
- ,
36 | order: usize,
37 | ) -> Result, String>
38 | where
39 | T: Debug + Display + ComplexField + PolynomialCoef + Espilon,
40 | Q: ComplexFloat + Scalar + Debug + Espilon,
41 | {
42 | let x = x.into_iter().collect::>();
43 | let y = y.into_iter().collect::>();
44 |
45 | if x.len() != y.len() {
46 | return Err(format!(
47 | "lsq_linear failed due to: len of x: {} and y: {} are not equal",
48 | x.len(),
49 | y.len()
50 | ));
51 | }
52 |
53 | let y = nalgebra::DVector::from_iterator(y.len(), y.into_iter().cloned());
54 |
55 | let rows = (0..x.len())
56 | .map(move |i| {
57 | nalgebra::DVector::from_iterator(
58 | order + 1,
59 | (0..(order + 1)).map(|a| ComplexFloat::powi(*x[i], a as i32)),
60 | )
61 | .transpose()
62 | })
63 | .collect::>();
64 |
65 | nalgebra::DMatrix::from_rows(rows.as_slice())
66 | .svd(true, true)
67 | .solve(&y, Q::epsilon())
68 | .map(|s| s.into_iter().cloned().collect())
69 | .map_err(|e| format!("lsq_linear failed due to: {}", e))
70 | }
71 |
--------------------------------------------------------------------------------
/src/optimize/metric.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{Debug, Display};
2 | use std::ops::{Add, Sub};
3 | use std::rc::Rc;
4 |
5 | use num::complex::{Complex32, Complex64, ComplexFloat};
6 | use num::traits::FloatConst;
7 | use num::{Complex, Float, NumCast, One, Zero};
8 |
9 | /// # `Metric`
10 | /// `Metric` is trait for float, which all compare the optimizing solution to the allowed tolerance
11 | ///
12 | /// It is implemented for `f32` and `f64`
13 | pub trait Metric: Float + Sized + Clone + Debug {}
14 | impl Metric for f32 {}
15 | impl Metric for f64 {}
16 |
17 | /// # MetricType
18 | /// Different type of method to measure the norm of a certain type
19 | #[derive(Clone)]
20 | pub enum MetricType
21 | where
22 | T: IntoMetric,
23 | M: Metric,
24 | {
25 | /// powered sum of all element
26 | PowerSum(M),
27 | /// L1 norm
28 | L1Norm,
29 | /// L2 norm
30 | L2Norm,
31 | /// p norm
32 | PNorm(M),
33 | /// mean square
34 | MS,
35 | /// root mean square
36 | RMS,
37 | /// custom function
38 | Custom(Rc M>),
39 | }
40 |
41 | impl Debug for MetricType
42 | where
43 | T: IntoMetric,
44 | M: Metric + Debug,
45 | {
46 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 | f.write_fmt(format_args!("MetricType::{}", {
48 | match self {
49 | Self::PNorm(p) => format!("PNorm-{:?}", p),
50 | Self::L1Norm => "L1-Norm".to_string(),
51 | Self::L2Norm => "L2-Norm".to_string(),
52 | Self::MS => "MS".to_string(),
53 | Self::RMS => "RMS".to_string(),
54 | Self::PowerSum(p) => format!("PowerSum-{:?}", p),
55 | Self::Custom(_) => "Custom".to_string(),
56 | }
57 | }))
58 | }
59 | }
60 |
61 | /// # `IntoMetric`
62 | /// `IntoMetric` is a trait for evaluating optimization solution.
63 | ///
64 | /// Since optimization solution is not always comparable with the specified tolerance,
65 | /// e.g. the solution with type `Array` and tolerance metric with type `f64`
66 | ///
67 | /// this trait allow all optimizaition solution to be turn into a `Metric`. Implemented for:
68 | /// ```ignore
69 | /// f32, f64, Complex32, Complex64, Array1, Array1, Array1, Array1
70 | /// ```
71 | ///
72 | /// Type with this trait must meet bound `Sub