From 3392f668d5a1f4c961a93e63495b20925e0cc682 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 18 Aug 2025 12:44:24 +0200 Subject: [PATCH 01/12] Integrate AD functionalities from num-dual --- Cargo.toml | 16 +- example/extend_quantity/Cargo.toml | 2 +- si-units/Cargo.toml | 4 +- src/lib.rs | 77 +------ src/nalgebra.rs | 131 +++++++++++ src/num_dual.rs | 359 +++++++++++++++++++++++++++++ src/ops.rs | 98 ++++++++ src/python.rs | 113 ++++++++- 8 files changed, 717 insertions(+), 83 deletions(-) create mode 100644 src/nalgebra.rs create mode 100644 src/num_dual.rs diff --git a/Cargo.toml b/Cargo.toml index 53710b6..7fe6cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,17 +29,21 @@ num-traits = "0.2" document-features = "0.2" ## Use N-dimensional arrays from the [ndarray] crate as value of a quantity. ndarray = { version = "0.16", optional = true } +## Use dynamic or static arrays from the [nalgebra] crate as value of a quantity. +nalgebra = { version = "0.33", optional = true } approx = { version = "0.5", optional = true } -pyo3 = { version = "0.23", optional = true } -numpy = { version = "0.23", optional = true } -## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. -num-dual = { version = "0.11.2", optional = true } +pyo3 = { version = "0.25", optional = true } +numpy = { version = "0.25", optional = true } +num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } +nshare = { version = "0.10", optional = true } [features] -default = [] +default = ["num-dual", "approx"] +## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. +num-dual = ["dep:num-dual", "nalgebra"] ## Directly use (scalar) quantities in Python interfaces through [pyo3] and the [si-units](https://pypi.org/project/si-units/) package. python = ["pyo3"] ## Use scalar and array quantities in Python interfaces through [pyo3], [numpy], and the [si-units](https://pypi.org/project/si-units/) package. -python_numpy = ["python", "numpy", "ndarray"] +python_numpy = ["python", "numpy/nalgebra", "ndarray", "nalgebra", "nshare"] ## Enable approximate comparisons through the [approx] crate. approx = ["dep:approx", "ndarray?/approx"] diff --git a/example/extend_quantity/Cargo.toml b/example/extend_quantity/Cargo.toml index 886503e..8302069 100644 --- a/example/extend_quantity/Cargo.toml +++ b/example/extend_quantity/Cargo.toml @@ -8,6 +8,6 @@ name = "extend_quantity" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] } +pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } quantity = { version = "*", path = "../../", features = ["python_numpy"] } ndarray = "0.16" diff --git a/si-units/Cargo.toml b/si-units/Cargo.toml index bc45d70..6db9db7 100644 --- a/si-units/Cargo.toml +++ b/si-units/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["cdylib"] [dependencies] ndarray = "0.16" -numpy = "0.23" -pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] } +numpy = "0.25" +pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } regex = "1.11" thiserror = "2.0" diff --git a/src/lib.rs b/src/lib.rs index b3cd56f..862846b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -151,6 +151,10 @@ use typenum::{ATerm, Diff, Integer, Negate, Quot, Sum, TArr, N1, N2, P1, P3, Z0} #[cfg(feature = "ndarray")] mod array; mod fmt; +#[cfg(feature = "nalgebra")] +mod nalgebra; +#[cfg(feature = "num-dual")] +pub mod num_dual; mod ops; #[cfg(feature = "python")] mod python; @@ -519,31 +523,6 @@ impl Deref for Dimensionless { } } -#[cfg(feature = "num-dual")] -mod num_dual { - use super::Quantity; - use num_dual::{DualNum, DualStruct}; - - impl, U> Quantity { - pub fn re(&self) -> Quantity { - Quantity::new(self.0.re()) - } - } - - impl, U> DualStruct for Quantity { - type Real = Quantity; - type Lifted> = Quantity, U>; - - fn real(&self) -> Self::Real { - Quantity::new(self.0.real()) - } - - fn lift>(&self) -> Self::Lifted { - Quantity::new(self.0.lift()) - } - } -} - #[cfg(test)] mod test { use super::*; @@ -555,51 +534,3 @@ mod test { assert_eq!(x, 1.0135e5_f64.ln()) } } - -#[cfg(test)] -#[cfg(feature = "num-dual")] -mod hmm { - use super::*; - use ::num_dual::{Dual64, DualNum, DualStruct, ImplicitDerivative, ImplicitFunction}; - - struct MyArgs { - temperature: Temperature, - } - - impl + Copy> DualStruct for MyArgs { - type Real = MyArgs; - - type Lifted> = MyArgs>; - - fn real(&self) -> Self::Real { - MyArgs { - temperature: self.temperature.real(), - } - } - - fn lift>(&self) -> Self::Lifted { - MyArgs { - temperature: self.temperature.lift(), - } - } - } - - struct AreaImplicit; - impl ImplicitFunction for AreaImplicit { - type Parameters> = Area; - type Variable = D; - fn residual + Copy>(&area: &Area, x: D) -> D { - let l = Length::new(x); - (area - l * l).convert_into(area) - } - } - - #[test] - fn test_implicit() { - let a = Area::new(Dual64::from(25.0).derivative()); - let implicit = ImplicitDerivative::new(AreaImplicit, a); - let x = implicit.implicit_derivative(5.0); - println!("{x}"); - assert_eq!(x, a.0.sqrt()) - } -} diff --git a/src/nalgebra.rs b/src/nalgebra.rs new file mode 100644 index 0000000..aa6014d --- /dev/null +++ b/src/nalgebra.rs @@ -0,0 +1,131 @@ +use super::Quantity; +use nalgebra::allocator::Allocator; +use nalgebra::constraint::{DimEq, ShapeConstraint}; +use nalgebra::{ClosedAddAssign, ClosedMulAssign, DMatrix, DefaultAllocator, Dim, OMatrix, Scalar}; +use num_traits::Zero; +use std::marker::PhantomData; +use std::ops::Add; +use typenum::Sum; + +impl Quantity, U> +where + DefaultAllocator: Allocator, +{ + /// Return the total number of elements in the matrix. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Return whether the matrix has any elements + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return the sum of all elements in the matrix. + /// + /// # Example + /// ``` + /// # use quantity::BAR; + /// # use nalgebra::dvector; + /// # use approx::assert_relative_eq; + /// let x = dvector![1.5, 2.5] * BAR; + /// assert_relative_eq!(x.sum(), &(4.0 * BAR)); + /// ``` + pub fn sum(&self) -> Quantity + where + T: Zero + ClosedAddAssign, + { + Quantity(self.0.sum(), PhantomData) + } + + pub fn get(&self, index: usize) -> Quantity + where + T: Copy, + { + Quantity(self.0[index], PhantomData) + } + + pub fn set(&mut self, index: usize, value: Quantity) { + self.0[index] = value.0; + } + + pub fn get2(&self, i: usize, j: usize) -> Quantity + where + T: Copy, + { + Quantity(self.0[(i, j)], PhantomData) + } + + pub fn set2(&mut self, i: usize, j: usize, value: Quantity) { + self.0[(i, j)] = value.0; + } + + pub fn shape_generic(&self) -> (R, C) { + self.0.shape_generic() + } +} + +impl Quantity, U> +where + DefaultAllocator: Allocator, +{ + pub fn add_scalar(&self, rhs: Quantity) -> Self + where + T: ClosedAddAssign, + { + Self(self.0.add_scalar(rhs.0), PhantomData) + } + + pub fn component_mul( + &self, + rhs: &Quantity, U2>, + ) -> Quantity, Sum> + where + T: ClosedMulAssign, + U: Add, + { + Quantity(self.0.component_mul(&rhs.0), PhantomData) + } + + pub fn dot( + &self, + rhs: &Quantity, U2>, + ) -> Quantity> + where + DefaultAllocator: Allocator, + T: Zero + ClosedAddAssign + ClosedMulAssign, + U: Add, + ShapeConstraint: DimEq + DimEq, + { + Quantity(self.0.dot(&rhs.0), PhantomData) + } + + pub fn from_fn_generic(nrows: R, ncols: C, mut f: F) -> Self + where + F: FnMut(usize, usize) -> Quantity, + { + Self( + OMatrix::from_fn_generic(nrows, ncols, |i, j| f(i, j).0), + PhantomData, + ) + } + + pub fn from_element_generic(nrows: R, ncols: C, elem: Quantity) -> Self { + Self( + OMatrix::from_element_generic(nrows, ncols, elem.0), + PhantomData, + ) + } +} + +impl Quantity, U> { + pub fn from_fn(nrows: usize, ncols: usize, mut f: F) -> Self + where + F: FnMut(usize, usize) -> Quantity, + { + Self( + DMatrix::from_fn(nrows, ncols, |i, j| f(i, j).0), + PhantomData, + ) + } +} diff --git a/src/num_dual.rs b/src/num_dual.rs new file mode 100644 index 0000000..c0c930a --- /dev/null +++ b/src/num_dual.rs @@ -0,0 +1,359 @@ +use super::Quantity; +use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, OMatrix, OVector, U1}; +use num_dual::{ + Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, HyperDual, HyperDualVec, + HyperHyperDual, +}; +use std::ops::Sub; +use typenum::Diff; + +impl, U> DualStruct for Quantity { + type Real = Quantity; + type Inner = Quantity; + + fn re(&self) -> Self::Real { + Quantity::new(self.0.re()) + } + + fn from_inner(inner: &Self::Inner) -> Self { + Quantity::new(T::from_inner(&inner.0)) + } +} + +pub fn first_derivative, UX, UY>( + g: G, + x: Quantity, +) -> (Quantity, Quantity>) +where + G: Fn(Quantity, UX>) -> Quantity, UY>, + UY: Sub, +{ + let r = num_dual::first_derivative(|x| g(Quantity::new(x)).0, x.0); + (Quantity::new(r.0), Quantity::new(r.1)) +} + +#[expect(clippy::type_complexity)] +pub fn gradient, UX, UY, N: Dim>( + g: G, + x: &Quantity, UX>, +) -> (Quantity, Quantity, Diff>) +where + DefaultAllocator: Allocator, + G: Fn(Quantity, N>, UX>) -> Quantity, UY>, + UY: Sub, +{ + let r = num_dual::gradient(|x| g(Quantity::new(x)).0, &x.0); + (Quantity::new(r.0), Quantity::new(r.1)) +} + +#[expect(clippy::type_complexity)] +pub fn jacobian, UX, UY, M: Dim, N: Dim>( + g: G, + x: Quantity, UX>, +) -> ( + Quantity, UY>, + Quantity, Diff>, +) +where + DefaultAllocator: Allocator + Allocator + Allocator + Allocator, + G: Fn( + Quantity, N>, UX>, + ) -> Quantity, M>, UY>, + UY: Sub, +{ + let r = num_dual::jacobian(|x| g(Quantity::new(x)).0, x.0); + (Quantity::new(r.0), Quantity::new(r.1)) +} + +#[expect(clippy::type_complexity)] +pub fn second_derivative, UX, UY>( + g: G, + x: Quantity, +) -> ( + Quantity, + Quantity>, + Quantity, UX>>, +) +where + G: Fn(Quantity, UX>) -> Quantity, UY>, + UY: Sub, + Diff: Sub, +{ + let r = num_dual::second_derivative(|x| g(Quantity::new(x)).0, x.0); + (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2)) +} + +#[expect(clippy::type_complexity)] +pub fn hessian, UX, UY, N: Dim>( + g: G, + x: &Quantity, UX>, +) -> ( + Quantity, + Quantity, Diff>, + Quantity, Diff, UX>>, +) +where + DefaultAllocator: Allocator + Allocator + Allocator, + G: Fn(Quantity, N>, UX>) -> Quantity, UY>, + UY: Sub, + Diff: Sub, +{ + let r = num_dual::hessian(|x| g(Quantity::new(x)).0, &x.0); + (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2)) +} + +#[expect(clippy::type_complexity)] +pub fn second_partial_derivative, UX, UY, UZ>( + g: G, + (x, y): (Quantity, Quantity), +) -> ( + Quantity, + Quantity>, + Quantity>, + Quantity, UY>>, +) +where + G: Fn( + ( + Quantity, UX>, + Quantity, UY>, + ), + ) -> Quantity, UZ>, + UZ: Sub, + UZ: Sub, + Diff: Sub, +{ + let r = num_dual::second_partial_derivative( + |(x, y)| g((Quantity::new(x), Quantity::new(y))).0, + (x.0, y.0), + ); + ( + Quantity::new(r.0), + Quantity::new(r.1), + Quantity::new(r.2), + Quantity::new(r.3), + ) +} + +#[expect(clippy::type_complexity)] +pub fn partial_hessian, UX, UY, UZ, M: Dim, N: Dim>( + g: G, + (x, y): (&Quantity, UX>, &Quantity, UY>), +) -> ( + Quantity, + Quantity, Diff>, + Quantity, Diff>, + Quantity, Diff, UY>>, +) +where + G: Fn( + ( + Quantity, M>, UX>, + Quantity, N>, UY>, + ), + ) -> Quantity, UZ>, + DefaultAllocator: Allocator + Allocator + Allocator + Allocator, + UZ: Sub, + UZ: Sub, + Diff: Sub, +{ + let r = num_dual::partial_hessian( + |(x, y)| g((Quantity::new(x), Quantity::new(y))).0, + (&x.0, &y.0), + ); + ( + Quantity::new(r.0), + Quantity::new(r.1), + Quantity::new(r.2), + Quantity::new(r.3), + ) +} + +#[expect(clippy::type_complexity)] +pub fn third_derivative, UX, UY>( + g: G, + x: Quantity, +) -> ( + Quantity, + Quantity>, + Quantity, UX>>, + Quantity, UX>, UX>>, +) +where + G: Fn(Quantity, UX>) -> Quantity, UY>, + UY: Sub, + Diff: Sub, + Diff, UX>: Sub, +{ + let r = num_dual::third_derivative(|x| g(Quantity::new(x)).0, x.0); + ( + Quantity::new(r.0), + Quantity::new(r.1), + Quantity::new(r.2), + Quantity::new(r.3), + ) +} + +#[expect(clippy::type_complexity)] +pub fn third_partial_derivative, UX, UY, UZ, U>( + g: G, + (x, y, z): (Quantity, Quantity, Quantity), +) -> ( + Quantity, + Quantity>, + Quantity>, + Quantity>, + Quantity, UY>>, + Quantity, UY>>, + Quantity, UY>>, + Quantity, UY>, UZ>>, +) +where + G: Fn( + ( + Quantity, UX>, + Quantity, UY>, + Quantity, UZ>, + ), + ) -> Quantity, UY>, + U: Sub, + U: Sub, + U: Sub, + Diff: Sub, + Diff: Sub, + Diff: Sub, + Diff, UY>: Sub, +{ + let r = num_dual::third_partial_derivative( + |(x, y, z)| g((Quantity::new(x), Quantity::new(y), Quantity::new(z))).0, + (x.0, y.0, z.0), + ); + ( + Quantity::new(r.0), + Quantity::new(r.1), + Quantity::new(r.2), + Quantity::new(r.3), + Quantity::new(r.4), + Quantity::new(r.5), + Quantity::new(r.6), + Quantity::new(r.7), + ) +} + +#[cfg(test)] +mod test_num_dual { + use super::*; + use crate::{Area, Length, Temperature, Volume, METER}; + use approx::assert_relative_eq; + use nalgebra::{vector, SMatrix, SVector}; + use num_dual::{Dual64, ImplicitDerivative, ImplicitFunction}; + use typenum::{P2, P3}; + + struct MyArgs { + temperature: Temperature, + } + + impl + Copy> DualStruct for MyArgs { + type Real = MyArgs; + type Inner = MyArgs; + + fn re(&self) -> Self::Real { + MyArgs { + temperature: self.temperature.re(), + } + } + + fn from_inner(inner: &Self::Inner) -> Self { + MyArgs { + temperature: Temperature::from_inner(&inner.temperature), + } + } + } + + struct AreaImplicit; + impl ImplicitFunction for AreaImplicit { + type Parameters = Area; + type Variable = D; + fn residual + Copy>(x: D, &area: &Area) -> D { + let l = Length::new(x); + (area - l * l).convert_into(area) + } + } + + #[test] + fn test_implicit() { + let a = Area::new(Dual64::from(25.0).derivative()); + let implicit = ImplicitDerivative::new(AreaImplicit, a); + let x = implicit.implicit_derivative(5.0); + println!("{x}"); + assert_eq!(x, a.0.sqrt()) + } + + fn volume + Copy>(x: Length) -> Volume { + x * x * x + } + + fn distance, N: Dim>(x: Length>) -> Length + where + DefaultAllocator: Allocator, + { + x.dot(&x).sqrt() + } + + fn volume2 + Copy>((x, h): (Length, Length)) -> Volume { + x * x * h + } + + #[test] + fn test_derivative() { + let (v, dv) = first_derivative(volume, 5.0 * METER); + println!("{v}\t{dv:3}"); + assert_eq!(v, 125.0 * METER.powi::()); + assert_eq!(dv, 75.0 * METER.powi::()); + + let (v, dv, d2v) = second_derivative(volume, 5.0 * METER); + println!("{v}\t{dv:3}\t\t{d2v}"); + assert_eq!(v, 125.0 * METER.powi::(),); + assert_eq!(dv, 75.0 * METER.powi::(),); + assert_eq!(d2v, 30.0 * METER); + + let (v, dv_dx, dv_dh, d2v) = + second_partial_derivative(volume2, (5.0 * METER, 20.0 * METER)); + println!("{v}\t{dv_dx:3}\t{dv_dh:3}\t{d2v}"); + assert_eq!(v, 500.0 * METER.powi::(),); + assert_eq!(dv_dx, 200.0 * METER.powi::(),); + assert_eq!(dv_dh, 25.0 * METER.powi::(),); + assert_eq!(d2v, 10.0 * METER); + + let (v, dv, d2v, d3v) = third_derivative(volume, 5.0 * METER); + println!("{v}\t{dv:3}\t\t{d2v}\t{d3v}"); + assert_eq!(v, 125.0 * METER.powi::(),); + assert_eq!(dv, 75.0 * METER.powi::(),); + assert_eq!(d2v, 30.0 * METER); + assert_eq!(d3v.into_value(), 6.0); + } + + #[test] + fn test_gradient_and_hessian() { + let x = vector![1.0, 5.0, 5.0, 7.0]; + let (d, grad) = gradient(distance, &Length::new(x)); + println!("{d}\t{grad:.1}"); + assert_eq!(d, 10.0 * METER); + assert_relative_eq!(grad.into_value(), SVector::from([0.1, 0.5, 0.5, 0.7])); + + let x = vector![1.0, 5.0, 5.0, 7.0]; + let (d, grad, hess) = hessian(distance, &Length::new(x)); + println!("{d}\t{grad:.1}\t{hess:.3?}"); + assert_eq!(d, 10.0 * METER); + assert_relative_eq!(grad.into_value(), SVector::from([0.1, 0.5, 0.5, 0.7])); + assert_relative_eq!( + (hess * METER).into_value(), + SMatrix::from([ + [0.099, -0.005, -0.005, -0.007], + [-0.005, 0.075, -0.025, -0.035], + [-0.005, -0.025, 0.075, -0.035], + [-0.007, -0.035, -0.035, 0.051] + ]) + ); + } +} diff --git a/src/ops.rs b/src/ops.rs index 4ef63b0..874bf74 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -1,8 +1,14 @@ use super::Quantity; #[cfg(feature = "approx")] use approx::{AbsDiffEq, RelativeEq}; +#[cfg(feature = "nalgebra")] +use nalgebra::allocator::Allocator; +#[cfg(feature = "nalgebra")] +use nalgebra::{DefaultAllocator, Dim, OMatrix}; #[cfg(feature = "ndarray")] use ndarray::{Array, ArrayBase, Data, DataMut, DataOwned, Dimension}; +#[cfg(feature = "num-dual")] +use num_dual::DualNum; use num_traits::{Inv, Signed}; use std::marker::PhantomData; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; @@ -98,6 +104,28 @@ impl + DataMut, D: Dimension> Mul> } } +#[cfg(feature = "nalgebra")] +impl Mul> for &OMatrix +where + DefaultAllocator: Allocator, +{ + type Output = Quantity, U>; + fn mul(self, other: Quantity) -> Self::Output { + Quantity(self * other.0, PhantomData) + } +} + +#[cfg(feature = "nalgebra")] +impl Mul> for OMatrix +where + DefaultAllocator: Allocator, +{ + type Output = Quantity, U>; + fn mul(self, other: Quantity) -> Self::Output { + Quantity(self * other.0, PhantomData) + } +} + impl MulAssign for Quantity where T1: MulAssign, @@ -336,6 +364,7 @@ where } } +#[cfg(not(feature = "num-dual"))] impl Quantity { /// Calculate the integer power of self. /// @@ -404,6 +433,75 @@ impl Quantity { } } +#[cfg(feature = "num-dual")] +impl, U> Quantity { + /// Calculate the integer power of self. + /// + /// # Example + /// ``` + /// # use quantity::METER; + /// # use approx::assert_relative_eq; + /// # use typenum::P2; + /// let x = 3.0 * METER; + /// assert_relative_eq!(x.powi::(), 9.0 * METER * METER); + /// ``` + pub fn powi(self) -> Quantity> + where + U: Mul, + { + Quantity(self.0.powi(E::I32), PhantomData) + } + + /// Calculate the square root of self. + /// + /// # Example + /// ``` + /// # use quantity::METER; + /// # use approx::assert_relative_eq; + /// let x = 9.0 * METER * METER; + /// assert_relative_eq!(x.sqrt(), 3.0 * METER); + /// ``` + pub fn sqrt(self) -> Quantity> + where + U: Div, + { + Quantity(self.0.sqrt(), PhantomData) + } + + /// Calculate the cubic root of self. + /// + /// # Example + /// ``` + /// # use quantity::METER; + /// # use approx::assert_relative_eq; + /// let x = 27.0 * METER * METER * METER; + /// assert_relative_eq!(x.cbrt(), 3.0 * METER); + /// ``` + pub fn cbrt(self) -> Quantity> + where + U: Div, + { + Quantity(self.0.cbrt(), PhantomData) + } + + /// Calculate the integer root of self. + /// + /// # Example + /// ``` + /// # use quantity::METER; + /// # use approx::assert_relative_eq; + /// # use typenum::P4; + /// let x = 81.0 * METER * METER * METER * METER; + /// assert_relative_eq!(x.root::(), 3.0 * METER); + /// ``` + pub fn root(self) -> Quantity> + where + U: Div, + { + Quantity(self.0.powf(1.0 / R::I32 as f64), PhantomData) + } +} + impl Quantity { /// Return the absolute value of `self`. /// diff --git a/src/python.rs b/src/python.rs index 226ed3b..9d2f7a9 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,9 +1,17 @@ use super::{Angle, Quantity, SIUnit}; use crate::fmt::PrintUnit; +#[cfg(feature = "nalgebra")] +use nalgebra::{allocator::Allocator, DMatrix, DVector, DefaultAllocator, Dim, OMatrix}; #[cfg(feature = "ndarray")] use ndarray::{Array, Dimension}; +#[cfg(feature = "nalgebra")] +use nshare::IntoNalgebra; #[cfg(feature = "ndarray")] -use numpy::{IntoPyArray, PyReadonlyArray}; +use numpy::IntoPyArray; +#[cfg(any(feature = "nalgebra", feature = "ndarray"))] +use numpy::PyReadonlyArray; +#[cfg(feature = "nalgebra")] +use numpy::{PyArrayMethods, PyReadonlyArray1, PyReadonlyArray2, ToPyArray}; use pyo3::{exceptions::PyValueError, prelude::*}; use std::{marker::PhantomData, sync::LazyLock}; use typenum::Integer; @@ -63,6 +71,33 @@ impl< } } +#[cfg(feature = "nalgebra")] +impl< + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + R: Dim, + C: Dim, + > IntoPyObject<'py> for Quantity, SIUnit> +where + DefaultAllocator: Allocator, +{ + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult> { + let unit = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; + let value = self.0.to_pyarray(py).into_any(); + SIOBJECT.bind(py).call1((value, unit)) + } +} + impl< 'py, T: Integer, @@ -140,6 +175,82 @@ where } } +#[cfg(feature = "nalgebra")] +impl< + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + > FromPyObject<'py> for Quantity, SIUnit> +where + Self: PrintUnit, +{ + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| { + raw.extract::<(PyReadonlyArray1, [i8; 7])>() + .map(|(m, u)| (m.to_owned_array(), u)) + }) else { + return Err(PyErr::new::(format!( + "Missing units! Expected {}, got {}.", + Self::UNIT, + ob.call_method0("__repr__")? + ))); + }; + let unit_into = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; + if unit_into == unit_from { + Ok(Quantity(value.into_nalgebra(), PhantomData)) + } else { + Err(PyErr::new::(format!( + "Wrong units! Expected {}, got {}.", + Self::UNIT, + ob.call_method0("__repr__")? + ))) + } + } +} + +#[cfg(feature = "nalgebra")] +impl< + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + > FromPyObject<'py> for Quantity, SIUnit> +where + Self: PrintUnit, +{ + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| { + raw.extract::<(PyReadonlyArray2, [i8; 7])>() + .map(|(m, u)| (m.to_owned_array(), u)) + }) else { + return Err(PyErr::new::(format!( + "Missing units! Expected {}, got {}.", + Self::UNIT, + ob.call_method0("__repr__")? + ))); + }; + let unit_into = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; + if unit_into == unit_from { + Ok(Quantity(value.into_nalgebra(), PhantomData)) + } else { + Err(PyErr::new::(format!( + "Wrong units! Expected {}, got {}.", + Self::UNIT, + ob.call_method0("__repr__")? + ))) + } + } +} + static ANGLE: LazyLock = LazyLock::new(|| { Python::with_gil(|py| { PyModule::import(py, "si_units") From 284b479404851aa23138e18d149032d433d5e63c Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 18 Aug 2025 12:52:00 +0200 Subject: [PATCH 02/12] avoid naming conflicts --- Cargo.toml | 2 +- src/{num_dual.rs => ad.rs} | 0 src/lib.rs | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/{num_dual.rs => ad.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 7fe6cd7..0cf566f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_na nshare = { version = "0.10", optional = true } [features] -default = ["num-dual", "approx"] +default = [] ## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. num-dual = ["dep:num-dual", "nalgebra"] ## Directly use (scalar) quantities in Python interfaces through [pyo3] and the [si-units](https://pypi.org/project/si-units/) package. diff --git a/src/num_dual.rs b/src/ad.rs similarity index 100% rename from src/num_dual.rs rename to src/ad.rs diff --git a/src/lib.rs b/src/lib.rs index 862846b..7cb9f86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -148,13 +148,13 @@ use std::marker::PhantomData; use std::ops::{Deref, Div, Mul}; use typenum::{ATerm, Diff, Integer, Negate, Quot, Sum, TArr, N1, N2, P1, P3, Z0}; +#[cfg(feature = "num-dual")] +pub mod ad; #[cfg(feature = "ndarray")] mod array; mod fmt; #[cfg(feature = "nalgebra")] mod nalgebra; -#[cfg(feature = "num-dual")] -pub mod num_dual; mod ops; #[cfg(feature = "python")] mod python; From 08417e81db7b47404794889d70a88a7292ef28ab Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 18 Aug 2025 15:37:38 +0200 Subject: [PATCH 03/12] add approx to dev-dependencies --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 0cf566f..661137a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ numpy = { version = "0.25", optional = true } num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } nshare = { version = "0.10", optional = true } +[dev-dependencies] +approx = { version = "0.5" } + [features] default = [] ## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. From 8ef05b5ef72b5baf16a38aff8773c204b26dd15b Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 18 Aug 2025 15:39:39 +0200 Subject: [PATCH 04/12] add workflow runs for tests with specific features --- .github/workflows/rust.yml | 12 ++++++++++++ Cargo.toml | 3 --- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d3cae4f..5f0d33c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -32,3 +32,15 @@ jobs: - uses: actions/checkout@v4 - name: Run tests (ndarray) run: cargo test --release --features "ndarray approx" + test_nalgebra: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests (nalgebra) + run: cargo test --release --features "nalgebra approx" + test_num_dual: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests (num-dual) + run: cargo test --release --features "num-dual approx" diff --git a/Cargo.toml b/Cargo.toml index 661137a..0cf566f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,9 +37,6 @@ numpy = { version = "0.25", optional = true } num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } nshare = { version = "0.10", optional = true } -[dev-dependencies] -approx = { version = "0.5" } - [features] default = [] ## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. From 6558a5742f506a26be855b9c9e95522c20c0729c Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 25 Aug 2025 17:16:38 +0200 Subject: [PATCH 05/12] add `copy` versions of `gradient`, `hessian`, and `partial_hessian` --- src/ad.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/array.rs | 2 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/ad.rs b/src/ad.rs index c0c930a..7e9e508 100644 --- a/src/ad.rs +++ b/src/ad.rs @@ -1,7 +1,7 @@ use super::Quantity; use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, OMatrix, OVector, U1}; use num_dual::{ - Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, HyperDual, HyperDualVec, + Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, Gradients, HyperDual, HyperDualVec, HyperHyperDual, }; use std::ops::Sub; @@ -46,6 +46,20 @@ where (Quantity::new(r.0), Quantity::new(r.1)) } +#[expect(clippy::type_complexity)] +pub fn gradient_copy + Copy, UX, UY, N: Gradients>( + g: G, + x: &Quantity, UX>, +) -> (Quantity, Quantity, Diff>) +where + DefaultAllocator: Allocator, + G: Fn(Quantity, N>, UX>) -> Quantity, UY>, + UY: Sub, +{ + let r = N::gradient(|x, _: &()| g(Quantity::new(x)).0, &x.0, &()); + (Quantity::new(r.0), Quantity::new(r.1)) +} + #[expect(clippy::type_complexity)] pub fn jacobian, UX, UY, M: Dim, N: Dim>( g: G, @@ -102,6 +116,25 @@ where (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2)) } +#[expect(clippy::type_complexity)] +pub fn hessian_copy + Copy, UX, UY, N: Gradients>( + g: G, + x: &Quantity, UX>, +) -> ( + Quantity, + Quantity, Diff>, + Quantity, Diff, UX>>, +) +where + DefaultAllocator: Allocator + Allocator, + G: Fn(Quantity, N>, UX>) -> Quantity, UY>, + UY: Sub, + Diff: Sub, +{ + let r = N::hessian(|x, _: &()| g(Quantity::new(x)).0, &x.0, &()); + (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2)) +} + #[expect(clippy::type_complexity)] pub fn second_partial_derivative, UX, UY, UZ>( g: G, @@ -169,6 +202,42 @@ where ) } +#[expect(clippy::type_complexity)] +pub fn partial_hessian_copy + Copy, UX, UY, UZ, N: Gradients>( + g: G, + (x, y): (&Quantity, UX>, Quantity), +) -> ( + Quantity, + Quantity, Diff>, + Quantity>, + Quantity, Diff, UY>>, +) +where + G: Fn( + ( + Quantity, N>, UX>, + Quantity, UY>, + ), + ) -> Quantity, UZ>, + DefaultAllocator: Allocator, + UZ: Sub, + UZ: Sub, + Diff: Sub, +{ + let r = N::partial_hessian( + |x, y, _: &()| g((Quantity::new(x), Quantity::new(y))).0, + &x.0, + y.0, + &(), + ); + ( + Quantity::new(r.0), + Quantity::new(r.1), + Quantity::new(r.2), + Quantity::new(r.3), + ) +} + #[expect(clippy::type_complexity)] pub fn third_derivative, UX, UY>( g: G, diff --git a/src/array.rs b/src/array.rs index 9e1571d..966202b 100644 --- a/src/array.rs +++ b/src/array.rs @@ -142,7 +142,7 @@ impl, U, D: Dimension> Quantity, U> { } /// Return a producer and iterable that traverses over all 1D lanes pointing in the direction of axis. - pub fn lanes_mut(&mut self, axis: Axis) -> LanesMut + pub fn lanes_mut(&mut self, axis: Axis) -> LanesMut<'_, T, D::Smaller> where S: DataMut, { From 78d9c84280d0aef99bbe795fcc60b63e99e3580e Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Mon, 25 Aug 2025 18:13:31 +0200 Subject: [PATCH 06/12] remove nshare and add test for nalgebra in Python interface --- .github/workflows/test_python.yml | 2 +- Cargo.toml | 8 ++--- example/extend_quantity/Cargo.toml | 1 + example/extend_quantity/src/lib.rs | 9 ++++++ example/extend_quantity/test.py | 11 +++++++ src/python.rs | 49 ++++++++++++++++++++++-------- 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 18be9ca..ca6ced5 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -35,4 +35,4 @@ jobs: maturin build --release --out dist pip install si-units --no-index --find-links dist --force-reinstall - name: Test with pytest - run: pytest example/extend_quantity/test.py + run: pytest --doctest-modules example/extend_quantity/test.py diff --git a/Cargo.toml b/Cargo.toml index 0cf566f..ee131f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,12 +30,12 @@ document-features = "0.2" ## Use N-dimensional arrays from the [ndarray] crate as value of a quantity. ndarray = { version = "0.16", optional = true } ## Use dynamic or static arrays from the [nalgebra] crate as value of a quantity. -nalgebra = { version = "0.33", optional = true } +nalgebra = { version = "0.34", optional = true } approx = { version = "0.5", optional = true } pyo3 = { version = "0.25", optional = true } -numpy = { version = "0.25", optional = true } +#numpy = { version = "0.26", optional = true } +numpy = { git = "https://github.com/PyO3/rust-numpy", optional = true } num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } -nshare = { version = "0.10", optional = true } [features] default = [] @@ -44,6 +44,6 @@ num-dual = ["dep:num-dual", "nalgebra"] ## Directly use (scalar) quantities in Python interfaces through [pyo3] and the [si-units](https://pypi.org/project/si-units/) package. python = ["pyo3"] ## Use scalar and array quantities in Python interfaces through [pyo3], [numpy], and the [si-units](https://pypi.org/project/si-units/) package. -python_numpy = ["python", "numpy/nalgebra", "ndarray", "nalgebra", "nshare"] +python_numpy = ["python", "numpy/nalgebra", "ndarray", "nalgebra"] ## Enable approximate comparisons through the [approx] crate. approx = ["dep:approx", "ndarray?/approx"] diff --git a/example/extend_quantity/Cargo.toml b/example/extend_quantity/Cargo.toml index 8302069..010b954 100644 --- a/example/extend_quantity/Cargo.toml +++ b/example/extend_quantity/Cargo.toml @@ -11,3 +11,4 @@ crate-type = ["cdylib"] pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } quantity = { version = "*", path = "../../", features = ["python_numpy"] } ndarray = "0.16" +nalgebra = "0.34" \ No newline at end of file diff --git a/example/extend_quantity/src/lib.rs b/example/extend_quantity/src/lib.rs index e8e9384..bd6b677 100644 --- a/example/extend_quantity/src/lib.rs +++ b/example/extend_quantity/src/lib.rs @@ -2,6 +2,7 @@ use pyo3::pymodule; #[pymodule] mod extend_quantity { + use nalgebra::{DMatrix, DVector}; use ndarray::Array1; use pyo3::pyfunction; use quantity::*; @@ -34,4 +35,12 @@ mod extend_quantity { fn law_of_cosines2(a: Length, b: Length, c: Length) -> Angle { Angle::acos((a * a + b * b - c * c).convert_into(2.0 * a * b)) } + + #[pyfunction] + fn test_nalgebra( + pressure: Pressure>, + volume: Volume>, + ) -> Energy> { + pressure * volume + } } diff --git a/example/extend_quantity/test.py b/example/extend_quantity/test.py index 72909f6..63cf441 100644 --- a/example/extend_quantity/test.py +++ b/example/extend_quantity/test.py @@ -18,3 +18,14 @@ def test_array(): >>> ideal_gas_array(T, V, N) array([ 826.31900987, 1058.09851279]) Pa """ + +def test_nalgebra(): + """ + >>> from extend_quantity import test_nalgebra + >>> import si_units as si + >>> import numpy as np + >>> p = np.array([[1, 2], [3, 4]]) * si.BAR + >>> V = np.array([1, 2]) * si.LITER + >>> test_nalgebra(p, V) + array([ 500., 1100.]) J + """ diff --git a/src/python.rs b/src/python.rs index 9d2f7a9..3bfde3f 100644 --- a/src/python.rs +++ b/src/python.rs @@ -1,17 +1,15 @@ use super::{Angle, Quantity, SIUnit}; use crate::fmt::PrintUnit; #[cfg(feature = "nalgebra")] -use nalgebra::{allocator::Allocator, DMatrix, DVector, DefaultAllocator, Dim, OMatrix}; +use nalgebra::{DMatrix, DVector, Dyn}; #[cfg(feature = "ndarray")] use ndarray::{Array, Dimension}; -#[cfg(feature = "nalgebra")] -use nshare::IntoNalgebra; #[cfg(feature = "ndarray")] use numpy::IntoPyArray; #[cfg(any(feature = "nalgebra", feature = "ndarray"))] use numpy::PyReadonlyArray; #[cfg(feature = "nalgebra")] -use numpy::{PyArrayMethods, PyReadonlyArray1, PyReadonlyArray2, ToPyArray}; +use numpy::{PyReadonlyArray1, PyReadonlyArray2, ToPyArray}; use pyo3::{exceptions::PyValueError, prelude::*}; use std::{marker::PhantomData, sync::LazyLock}; use typenum::Integer; @@ -81,11 +79,7 @@ impl< THETA: Integer, N: Integer, J: Integer, - R: Dim, - C: Dim, - > IntoPyObject<'py> for Quantity, SIUnit> -where - DefaultAllocator: Allocator, + > IntoPyObject<'py> for Quantity, SIUnit> { type Target = PyAny; type Output = Bound<'py, PyAny>; @@ -98,6 +92,29 @@ where } } +#[cfg(feature = "nalgebra")] +impl< + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + > IntoPyObject<'py> for Quantity, SIUnit> +{ + type Target = PyAny; + type Output = Bound<'py, PyAny>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> PyResult> { + let unit = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; + let value = numpy::PyArray1::from_slice(py, self.0.data.as_vec()).into_any(); + SIOBJECT.bind(py).call1((value, unit)) + } +} + impl< 'py, T: Integer, @@ -192,7 +209,10 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| { raw.extract::<(PyReadonlyArray1, [i8; 7])>() - .map(|(m, u)| (m.to_owned_array(), u)) + .map(|(m, u)| { + let m: nalgebra::DVectorView = m.try_as_matrix().unwrap(); + (m.clone_owned(), u) + }) }) else { return Err(PyErr::new::(format!( "Missing units! Expected {}, got {}.", @@ -202,7 +222,7 @@ where }; let unit_into = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; if unit_into == unit_from { - Ok(Quantity(value.into_nalgebra(), PhantomData)) + Ok(Quantity(value, PhantomData)) } else { Err(PyErr::new::(format!( "Wrong units! Expected {}, got {}.", @@ -230,7 +250,10 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let Ok((value, unit_from)) = ob.call_method0("__getnewargs__").and_then(|raw| { raw.extract::<(PyReadonlyArray2, [i8; 7])>() - .map(|(m, u)| (m.to_owned_array(), u)) + .map(|(m, u)| { + let m: nalgebra::DMatrixView = m.try_as_matrix().unwrap(); + (m.clone_owned(), u) + }) }) else { return Err(PyErr::new::(format!( "Missing units! Expected {}, got {}.", @@ -240,7 +263,7 @@ where }; let unit_into = [L::I8, M::I8, T::I8, I::I8, N::I8, THETA::I8, J::I8]; if unit_into == unit_from { - Ok(Quantity(value.into_nalgebra(), PhantomData)) + Ok(Quantity(value, PhantomData)) } else { Err(PyErr::new::(format!( "Wrong units! Expected {}, got {}.", From 7dadcf86a8e68e428f946ebbe6fc6dae2309236d Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sat, 30 Aug 2025 15:55:45 +0200 Subject: [PATCH 07/12] update to pyo3 0.26 --- Cargo.toml | 5 ++--- example/extend_quantity/Cargo.toml | 2 +- si-units/Cargo.toml | 4 ++-- si-units/src/lib.rs | 8 ++++---- src/python.rs | 8 ++++---- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ee131f9..6980b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,8 @@ ndarray = { version = "0.16", optional = true } ## Use dynamic or static arrays from the [nalgebra] crate as value of a quantity. nalgebra = { version = "0.34", optional = true } approx = { version = "0.5", optional = true } -pyo3 = { version = "0.25", optional = true } -#numpy = { version = "0.26", optional = true } -numpy = { git = "https://github.com/PyO3/rust-numpy", optional = true } +pyo3 = { version = "0.26", optional = true } +numpy = { version = "0.26", optional = true } num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } [features] diff --git a/example/extend_quantity/Cargo.toml b/example/extend_quantity/Cargo.toml index 010b954..697d285 100644 --- a/example/extend_quantity/Cargo.toml +++ b/example/extend_quantity/Cargo.toml @@ -8,7 +8,7 @@ name = "extend_quantity" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } +pyo3 = { version = "0.26", features = ["extension-module", "abi3-py39"] } quantity = { version = "*", path = "../../", features = ["python_numpy"] } ndarray = "0.16" nalgebra = "0.34" \ No newline at end of file diff --git a/si-units/Cargo.toml b/si-units/Cargo.toml index 6db9db7..7378398 100644 --- a/si-units/Cargo.toml +++ b/si-units/Cargo.toml @@ -22,7 +22,7 @@ crate-type = ["cdylib"] [dependencies] ndarray = "0.16" -numpy = "0.25" -pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } +numpy = "0.26" +pyo3 = { version = "0.26", features = ["extension-module", "abi3-py39"] } regex = "1.11" thiserror = "2.0" diff --git a/si-units/src/lib.rs b/si-units/src/lib.rs index 109ee35..39ed6c6 100644 --- a/si-units/src/lib.rs +++ b/si-units/src/lib.rs @@ -28,12 +28,12 @@ pub enum QuantityError { #[pyclass(name = "SIObject", module = "si_units._core", frozen)] pub struct PySIObject { - value: PyObject, + value: Py, unit: SIUnit, } impl PySIObject { - fn new(value: PyObject, unit: SIUnit) -> Self { + fn new(value: Py, unit: SIUnit) -> Self { Self { value, unit } } @@ -93,7 +93,7 @@ impl PySIObject { .ok() } - fn __richcmp__(&self, py: Python, other: &Self, op: CompareOp) -> PyResult { + fn __richcmp__(&self, py: Python, other: &Self, op: CompareOp) -> PyResult> { self.check_units(other).and_then(|_| match op { CompareOp::Eq => self.value.call_method1(py, "__eq__", (&other.value,)), CompareOp::Ne => self.value.call_method1(py, "__ne__", (&other.value,)), @@ -240,7 +240,7 @@ impl PySIObject { } #[getter] - fn get_shape(&self, py: Python) -> PyResult { + fn get_shape(&self, py: Python) -> PyResult> { self.value.getattr(py, "shape") } diff --git a/src/python.rs b/src/python.rs index 3bfde3f..c8d0318 100644 --- a/src/python.rs +++ b/src/python.rs @@ -14,8 +14,8 @@ use pyo3::{exceptions::PyValueError, prelude::*}; use std::{marker::PhantomData, sync::LazyLock}; use typenum::Integer; -static SIOBJECT: LazyLock = LazyLock::new(|| { - Python::with_gil(|py| { +static SIOBJECT: LazyLock> = LazyLock::new(|| { + Python::attach(|py| { PyModule::import(py, "si_units") .unwrap() .getattr("SIObject") @@ -274,8 +274,8 @@ where } } -static ANGLE: LazyLock = LazyLock::new(|| { - Python::with_gil(|py| { +static ANGLE: LazyLock> = LazyLock::new(|| { + Python::attach(|py| { PyModule::import(py, "si_units") .unwrap() .getattr("Angle") From 777b9168a735d12bf8131543ef83f726f5eddad8 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Fri, 5 Sep 2025 12:28:24 +0200 Subject: [PATCH 08/12] add zeroth_derivative --- src/ad.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ad.rs b/src/ad.rs index 7e9e508..53b30d4 100644 --- a/src/ad.rs +++ b/src/ad.rs @@ -2,7 +2,7 @@ use super::Quantity; use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, OMatrix, OVector, U1}; use num_dual::{ Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, Gradients, HyperDual, HyperDualVec, - HyperHyperDual, + HyperHyperDual, Real, }; use std::ops::Sub; use typenum::Diff; @@ -20,6 +20,15 @@ impl, U> DualStruct for Quantity { } } +pub fn zeroth_derivative, UX, UY>(g: G, x: Quantity) -> Quantity +where + G: Fn(Quantity, UX>) -> Quantity, UY>, + UY: Sub, +{ + let r = num_dual::zeroth_derivative(|x| g(Quantity::new(x)).0, x.0); + Quantity::new(r) +} + pub fn first_derivative, UX, UY>( g: G, x: Quantity, From ec0c01b0ae2854d538c12b69b393a4327ed6d890 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Tue, 16 Sep 2025 13:42:38 +0200 Subject: [PATCH 09/12] update num-dual# --- src/ad.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/ad.rs b/src/ad.rs index 53b30d4..cd9d54a 100644 --- a/src/ad.rs +++ b/src/ad.rs @@ -72,7 +72,7 @@ where #[expect(clippy::type_complexity)] pub fn jacobian, UX, UY, M: Dim, N: Dim>( g: G, - x: Quantity, UX>, + x: &Quantity, UX>, ) -> ( Quantity, UY>, Quantity, Diff>, @@ -84,7 +84,24 @@ where ) -> Quantity, M>, UY>, UY: Sub, { - let r = num_dual::jacobian(|x| g(Quantity::new(x)).0, x.0); + let r = num_dual::jacobian(|x| g(Quantity::new(x)).0, &x.0); + (Quantity::new(r.0), Quantity::new(r.1)) +} + +#[expect(clippy::type_complexity)] +pub fn jacobian_copy + Copy, UX, UY, N: Gradients>( + g: G, + x: &Quantity, UX>, +) -> ( + Quantity, UY>, + Quantity, Diff>, +) +where + DefaultAllocator: Allocator + Allocator + Allocator + Allocator, + G: Fn(Quantity, N>, UX>) -> Quantity, N>, UY>, + UY: Sub, +{ + let r = N::jacobian(|x, _: &()| g(Quantity::new(x)).0, &x.0, &()); (Quantity::new(r.0), Quantity::new(r.1)) } From 9f4c54924050b8095a125e0418c2df88ad79d407 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 28 Sep 2025 15:33:57 +0200 Subject: [PATCH 10/12] prepare release v0.11.0 --- CHANGELOG.md | 7 ++++++- Cargo.toml | 8 ++++---- README.md | 12 ++++++------ src/lib.rs | 2 +- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c657e2a..8e85bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.11.0] - 2025-09-28 +### Added +- Added core functionalities for quantities with underlying data structures from `nalgebra`. [#92](https://github.com/itt-ustutt/quantity/pull/92) +- Added automatic differentiation capabilities from the `num-dual` crate. [#92](https://github.com/itt-ustutt/quantity/pull/92) + ## [0.10.6] - 2025-06-24 -## Changed +### Changed - Updated the optional num-dual dependency to 0.11.2. [#91](https://github.com/itt-ustutt/quantity/pull/91) ## [0.10.5] - 2025-06-03 diff --git a/Cargo.toml b/Cargo.toml index 6980b3b..8aa2dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "quantity" -version = "0.10.6" +version = "0.11.0" authors = [ "Philipp Rehner ", "Gernot Bauer ", ] -rust-version = "1.81" -edition = "2021" +rust-version = "1.87" +edition = "2024" license = "MIT OR Apache-2.0" description = "Representation of quantites, i.e. of unit valued scalars and arrays." homepage = "https://github.com/itt-ustutt/quantity" @@ -34,7 +34,7 @@ nalgebra = { version = "0.34", optional = true } approx = { version = "0.5", optional = true } pyo3 = { version = "0.26", optional = true } numpy = { version = "0.26", optional = true } -num-dual = { git = "https://github.com/itt-ustutt/num-dual", branch = "linalg_nalgebra", optional = true } +num-dual = { version = "0.12", optional = true } [features] default = [] diff --git a/README.md b/README.md index 78aeac1..631260e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Add this to your `Cargo.toml`: ``` [dependencies] -quantity = "0.10" +quantity = "0.11" ``` ## Examples @@ -24,7 +24,7 @@ Calculate pressure of an ideal gas. ```rust let temperature = 25.0 * CELSIUS; -let volume = 1.5 * METER.powi(3); +let volume = 1.5 * METER.powi::(); let moles = 75.0 * MOL; let pressure = moles * RGAS * temperature / volume; println!("{:.5}", pressure); // 123.94785 kPa @@ -36,19 +36,19 @@ Calculate the gravitational pull of the moon on the earth. let mass_earth = 5.9724e24 * KILOGRAM; let mass_moon = 7.346e22 * KILOGRAM; let distance = 383.398 * KILO * METER; -let force = G * mass_earth * mass_moon / distance.powi(2); +let force = G * mass_earth * mass_moon / distance.powi::(); println!("{:.5e}", force); // 1.99208e26 N ``` Calculate the pressure distribution in the atmosphere using the barometric formula. ```rust -let z = SIArray1::linspace(1.0 * METER, 70.0 * KILO * METER, 10)?; -let g = 9.81 * METER / SECOND.powi(2); +let z = Quantity::linspace(1.0 * METER, 70.0 * KILO * METER, 10); +let g = 9.81 * METER / SECOND.powi::(); let m = 28.949 * GRAM / MOL; let t = 10.0 * CELSIUS; let p0 = BAR; -let pressure = p0 * (-&z * m * g).to_reduced(RGAS * t)?.mapv(f64::exp); +let pressure = ((-z.clone() * m * g) / (RGAS * t)).mapv(f64::exp) * p0; for i in 0..10 { println!("z = {:8.5} p = {:9.5}", z.get(i), pressure.get(i)); } diff --git a/src/lib.rs b/src/lib.rs index 7cb9f86..bb9f58f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,7 @@ use ndarray::{Array, ArrayBase, Data, Dimension}; use std::marker::PhantomData; use std::ops::{Deref, Div, Mul}; -use typenum::{ATerm, Diff, Integer, Negate, Quot, Sum, TArr, N1, N2, P1, P3, Z0}; +use typenum::{ATerm, Diff, Integer, N1, N2, Negate, P1, P3, Quot, Sum, TArr, Z0}; #[cfg(feature = "num-dual")] pub mod ad; From 357568a3674d9cf6b6d29fd7ff2561b7c8821843 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 28 Sep 2025 15:36:12 +0200 Subject: [PATCH 11/12] rustfmt going crazy --- Cargo.toml | 2 +- src/ad.rs | 6 +-- src/fmt.rs | 20 ++++----- src/ops.rs | 2 +- src/python.rs | 112 +++++++++++++++----------------------------------- 5 files changed, 47 insertions(+), 95 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8aa2dd7..4096834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ numpy = { version = "0.26", optional = true } num-dual = { version = "0.12", optional = true } [features] -default = [] +default = ["python_numpy"] ## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. num-dual = ["dep:num-dual", "nalgebra"] ## Directly use (scalar) quantities in Python interfaces through [pyo3] and the [si-units](https://pypi.org/project/si-units/) package. diff --git a/src/ad.rs b/src/ad.rs index cd9d54a..367bfb2 100644 --- a/src/ad.rs +++ b/src/ad.rs @@ -1,5 +1,5 @@ use super::Quantity; -use nalgebra::{allocator::Allocator, DefaultAllocator, Dim, OMatrix, OVector, U1}; +use nalgebra::{DefaultAllocator, Dim, OMatrix, OVector, U1, allocator::Allocator}; use num_dual::{ Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, Gradients, HyperDual, HyperDualVec, HyperHyperDual, Real, @@ -338,9 +338,9 @@ where #[cfg(test)] mod test_num_dual { use super::*; - use crate::{Area, Length, Temperature, Volume, METER}; + use crate::{Area, Length, METER, Temperature, Volume}; use approx::assert_relative_eq; - use nalgebra::{vector, SMatrix, SVector}; + use nalgebra::{SMatrix, SVector, vector}; use num_dual::{Dual64, ImplicitDerivative, ImplicitFunction}; use typenum::{P2, P3}; diff --git a/src/fmt.rs b/src/fmt.rs index 3e35ef5..6b96098 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -4,20 +4,20 @@ use ndarray::{Array, Dimension}; use std::collections::HashMap; use std::fmt; use std::sync::LazyLock; -use typenum::{Quot, N1, N2, N3, P2, P3, P4}; +use typenum::{N1, N2, N3, P2, P3, P4, Quot}; const UNIT_SYMBOLS: [&str; 7] = ["s", "m", "kg", "A", "K", "mol", "cd"]; impl< - Inner: fmt::Debug, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > fmt::Debug for Quantity> + Inner: fmt::Debug, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, +> fmt::Debug for Quantity> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f)?; diff --git a/src/ops.rs b/src/ops.rs index 874bf74..8aaa9b1 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -12,7 +12,7 @@ use num_dual::DualNum; use num_traits::{Inv, Signed}; use std::marker::PhantomData; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; -use typenum::{Diff, Integer, Negate, Prod, Quot, Sum, P2, P3}; +use typenum::{Diff, Integer, Negate, P2, P3, Prod, Quot, Sum}; // Multiplication impl Mul> for Quantity diff --git a/src/python.rs b/src/python.rs index c8d0318..c1d145c 100644 --- a/src/python.rs +++ b/src/python.rs @@ -24,16 +24,8 @@ static SIOBJECT: LazyLock> = LazyLock::new(|| { }) }); -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > IntoPyObject<'py> for Quantity> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + IntoPyObject<'py> for Quantity> { type Target = PyAny; type Output = Bound<'py, PyAny>; @@ -47,16 +39,16 @@ impl< #[cfg(feature = "ndarray")] impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - D: Dimension, - > IntoPyObject<'py> for Quantity, SIUnit> + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + D: Dimension, +> IntoPyObject<'py> for Quantity, SIUnit> { type Target = PyAny; type Output = Bound<'py, PyAny>; @@ -70,16 +62,8 @@ impl< } #[cfg(feature = "nalgebra")] -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > IntoPyObject<'py> for Quantity, SIUnit> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + IntoPyObject<'py> for Quantity, SIUnit> { type Target = PyAny; type Output = Bound<'py, PyAny>; @@ -93,16 +77,8 @@ impl< } #[cfg(feature = "nalgebra")] -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > IntoPyObject<'py> for Quantity, SIUnit> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + IntoPyObject<'py> for Quantity, SIUnit> { type Target = PyAny; type Output = Bound<'py, PyAny>; @@ -115,16 +91,8 @@ impl< } } -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > FromPyObject<'py> for Quantity> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + FromPyObject<'py> for Quantity> where Self: PrintUnit, { @@ -154,16 +122,16 @@ where #[cfg(feature = "ndarray")] impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - D: Dimension, - > FromPyObject<'py> for Quantity, SIUnit> + 'py, + T: Integer, + L: Integer, + M: Integer, + I: Integer, + THETA: Integer, + N: Integer, + J: Integer, + D: Dimension, +> FromPyObject<'py> for Quantity, SIUnit> where Self: PrintUnit, { @@ -193,16 +161,8 @@ where } #[cfg(feature = "nalgebra")] -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > FromPyObject<'py> for Quantity, SIUnit> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + FromPyObject<'py> for Quantity, SIUnit> where Self: PrintUnit, { @@ -234,16 +194,8 @@ where } #[cfg(feature = "nalgebra")] -impl< - 'py, - T: Integer, - L: Integer, - M: Integer, - I: Integer, - THETA: Integer, - N: Integer, - J: Integer, - > FromPyObject<'py> for Quantity, SIUnit> +impl<'py, T: Integer, L: Integer, M: Integer, I: Integer, THETA: Integer, N: Integer, J: Integer> + FromPyObject<'py> for Quantity, SIUnit> where Self: PrintUnit, { From 70d4e635b0898a250766f4b116245b57a6ceb814 Mon Sep 17 00:00:00 2001 From: Philipp Rehner Date: Sun, 28 Sep 2025 15:55:28 +0200 Subject: [PATCH 12/12] bump min Rust versions to 1.87 --- Cargo.toml | 2 +- example/extend_quantity/Cargo.toml | 2 +- si-units/Cargo.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4096834..8aa2dd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ numpy = { version = "0.26", optional = true } num-dual = { version = "0.12", optional = true } [features] -default = ["python_numpy"] +default = [] ## Use generalized (hyper-)dual numbers from the [num-dual] crate as value of a quantity. num-dual = ["dep:num-dual", "nalgebra"] ## Directly use (scalar) quantities in Python interfaces through [pyo3] and the [si-units](https://pypi.org/project/si-units/) package. diff --git a/example/extend_quantity/Cargo.toml b/example/extend_quantity/Cargo.toml index 697d285..31fd5de 100644 --- a/example/extend_quantity/Cargo.toml +++ b/example/extend_quantity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "extend_quantity" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] name = "extend_quantity" diff --git a/si-units/Cargo.toml b/si-units/Cargo.toml index 7378398..ca2dae0 100644 --- a/si-units/Cargo.toml +++ b/si-units/Cargo.toml @@ -5,8 +5,8 @@ authors = [ "Philipp Rehner ", "Gernot Bauer ", ] -rust-version = "1.81" -edition = "2021" +rust-version = "1.87" +edition = "2024" license = "MIT OR Apache-2.0" description = "Representation of SI unit valued scalars and arrays." homepage = "https://github.com/itt-ustutt/quantity/tree/master/si-units"