From 422b2e45c8b62d53ac35ed50b6ebf111d2f2ec40 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:00:06 +0700 Subject: [PATCH 1/9] implement Primal --- crates/multicalc/src/scalar/mod.rs | 1 + crates/multicalc/src/scalar/primal.rs | 57 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 crates/multicalc/src/scalar/primal.rs diff --git a/crates/multicalc/src/scalar/mod.rs b/crates/multicalc/src/scalar/mod.rs index 535a31a..9fc1f44 100644 --- a/crates/multicalc/src/scalar/mod.rs +++ b/crates/multicalc/src/scalar/mod.rs @@ -9,6 +9,7 @@ pub mod function; pub mod hyper_dual; pub mod jet; pub mod numeric; +pub mod primal; pub use dual::Dual; pub use function::{ScalarFn, ScalarFnN, VectorFn, c}; diff --git a/crates/multicalc/src/scalar/primal.rs b/crates/multicalc/src/scalar/primal.rs new file mode 100644 index 0000000..728ee89 --- /dev/null +++ b/crates/multicalc/src/scalar/primal.rs @@ -0,0 +1,57 @@ +use crate::{Dual, HyperDual, Jet, Numeric}; + +pub trait Primal { + fn to_f64(&self) -> f64; + + fn to_f32(&self) -> f32; +} + +impl Primal for f64 { + fn to_f64(&self) -> f64 { + *self + } + + fn to_f32(&self) -> f32 { + *self as f32 + } +} + +impl Primal for f32 { + fn to_f64(&self) -> f64 { + *self as f64 + } + + fn to_f32(&self) -> f32 { + *self + } +} + +impl Primal for Dual { + fn to_f64(&self) -> f64 { + self.value.to_f64() + } + + fn to_f32(&self) -> f32 { + self.value.to_f32() + } +} + +impl Primal for HyperDual { + fn to_f64(&self) -> f64 { + self.real.to_f64() + } + + fn to_f32(&self) -> f32 { + self.real.to_f32() + } +} + +impl Primal for Jet { + fn to_f64(&self) -> f64 { + self.value().to_f64() + } + + fn to_f32(&self) -> f32 { + self.value().to_f32() + } +} From f8bbba0244bef609374af2613a4b61c0043105e7 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:06:41 +0700 Subject: [PATCH 2/9] use Primal --- showcase/viz/src/convert.rs | 44 ------------------------------------- showcase/viz/src/lib.rs | 3 +-- showcase/viz/src/sink.rs | 9 ++++---- 3 files changed, 6 insertions(+), 50 deletions(-) delete mode 100644 showcase/viz/src/convert.rs diff --git a/showcase/viz/src/convert.rs b/showcase/viz/src/convert.rs deleted file mode 100644 index dfa5216..0000000 --- a/showcase/viz/src/convert.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! One-way scalar-to-f64 projection for plotting. -//! -//! `multicalc::scalar::Numeric` provides `from_f64` but no `to_f64`, so this crate defines its -//! own projection. It covers the float scalars and the autodiff scalars, projecting each to its -//! primal (value) part so a differentiated quantity plots as its underlying value. The autodiff -//! impls delegate through the primal, so nested scalars such as `Dual>` work. - -use multicalc::scalar::{Dual, HyperDual, Jet, Numeric}; - -/// A scalar that can be projected to `f64` for plotting. -pub trait Plottable: Copy { - /// Returns the value as an `f64` (the primal part, for autodiff scalars). - fn to_plot_f64(self) -> f64; -} - -impl Plottable for f64 { - fn to_plot_f64(self) -> f64 { - self - } -} - -impl Plottable for f32 { - fn to_plot_f64(self) -> f64 { - f64::from(self) - } -} - -impl Plottable for Dual { - fn to_plot_f64(self) -> f64 { - self.value.to_plot_f64() - } -} - -impl Plottable for HyperDual { - fn to_plot_f64(self) -> f64 { - self.real.to_plot_f64() - } -} - -impl Plottable for Jet { - fn to_plot_f64(self) -> f64 { - self.value().to_plot_f64() - } -} diff --git a/showcase/viz/src/lib.rs b/showcase/viz/src/lib.rs index d85ac78..2b48c61 100644 --- a/showcase/viz/src/lib.rs +++ b/showcase/viz/src/lib.rs @@ -4,7 +4,6 @@ //! ([`RerunSink`], live or recorded) and a CSV backend ([`CsvSink`]) for the `plot.py` fallback. //! A satellite crate: never a dependency of the core, excluded from bare-metal builds. -mod convert; mod csv_sink; mod rerun_sink; mod sink; @@ -12,8 +11,8 @@ mod sink; #[doc(hidden)] pub mod loop_util; -pub use convert::Plottable; pub use csv_sink::CsvSink; +pub use multicalc::scalar::primal::Primal; pub use rerun_sink::RerunSink; pub use sink::{Rgba, VizError, VizSink, VizSinkExt}; diff --git a/showcase/viz/src/sink.rs b/showcase/viz/src/sink.rs index ce24077..2c3f98a 100644 --- a/showcase/viz/src/sink.rs +++ b/showcase/viz/src/sink.rs @@ -4,7 +4,8 @@ //! runtime; that is why `scalar` takes a plain `f64`. The generic convenience form that accepts //! any [`Plottable`] scalar lives on the blanket [`VizSinkExt`]. -use crate::convert::Plottable; +use multicalc::scalar::primal::Primal; + use core::fmt; /// An error from a sink backend. @@ -136,9 +137,9 @@ pub trait VizSink { /// Convenience extensions kept off the object-safe [`VizSink`]. pub trait VizSinkExt: VizSink { - /// Logs any [`Plottable`] scalar without an explicit `to_plot_f64`. - fn scalar_of(&mut self, path: &str, value: impl Plottable) -> Result<(), VizError> { - self.scalar(path, value.to_plot_f64()) + /// Logs any [`Primal`] scalar without an explicit `to_f64`. + fn scalar_of(&mut self, path: &str, value: impl Primal) -> Result<(), VizError> { + self.scalar(path, value.to_f64()) } } From 8ccb118a106ae0ef73b5a04f975cc7b9b9982a9e Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:06:47 +0700 Subject: [PATCH 3/9] add comments to Primal --- crates/multicalc/src/scalar/primal.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/multicalc/src/scalar/primal.rs b/crates/multicalc/src/scalar/primal.rs index 728ee89..d5e8d49 100644 --- a/crates/multicalc/src/scalar/primal.rs +++ b/crates/multicalc/src/scalar/primal.rs @@ -1,8 +1,18 @@ +//! One-way scalar-to-f64 projection for plotting. +//! +//! `multicalc::scalar::Numeric` provides `from_f64` but no `to_f64`, so this crate defines its +//! own projection. It covers the float scalars and the autodiff scalars, projecting each to its +//! primal (value) part so a differentiated quantity plots as its underlying value. The autodiff +//! impls delegate through the primal, so nested scalars such as `Dual>` work.use crate::{Dual, HyperDual, Jet, Numeric}; + use crate::{Dual, HyperDual, Jet, Numeric}; +/// A scalar that can be projected to `f64` and `f32`. pub trait Primal { + /// Returns the value as an `f64` (the primal part, for autodiff scalars). fn to_f64(&self) -> f64; + /// Returns the value as an `f32` (the primal part, for autodiff scalars). fn to_f32(&self) -> f32; } From a252e6e843373503be5e19f89474b58459204f9e Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:20:02 +0700 Subject: [PATCH 4/9] tests --- crates/multicalc/tests/scalar.rs | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/multicalc/tests/scalar.rs b/crates/multicalc/tests/scalar.rs index b97fa7b..2dcd186 100644 --- a/crates/multicalc/tests/scalar.rs +++ b/crates/multicalc/tests/scalar.rs @@ -77,6 +77,58 @@ mod numeric_methods { } } +mod primal { + use multicalc::{Dual, HyperDual, Jet, Numeric, scalar::primal::Primal}; + + #[test] + fn test_f64() { + let two = f64::TWO; + + assert_eq!(two.to_f64(), two); + assert_eq!(two.to_f32(), two as f32); + } + + #[test] + fn test_f32() { + let two = f32::TWO; + + assert_eq!(two.to_f64(), two as f64); + assert_eq!(two.to_f32(), two); + } + + #[test] + fn test_dual() { + let two = Dual::::TWO; + + assert_eq!(two.to_f64(), f64::TWO); + assert_eq!(two.to_f32(), f32::TWO); + } + + #[test] + fn test_hyperdual() { + let two = HyperDual::::TWO; + + assert_eq!(two.to_f64(), f64::TWO); + assert_eq!(two.to_f32(), f32::TWO); + } + + #[test] + fn test_jet() { + let two = Jet::::TWO; + + assert_eq!(two.to_f64(), f64::TWO); + assert_eq!(two.to_f32(), f32::TWO); + } + + #[test] + fn test_dual_hyperdual() { + let two = Dual::>::TWO; + + assert_eq!(two.to_f64(), f64::TWO); + assert_eq!(two.to_f32(), f32::TWO); + } +} + mod dual { use multicalc::scalar::Dual; use multicalc::scalar::Numeric; From b55377775bd11a54dddba2a8ce4be1e7fff1b123 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:30:46 +0700 Subject: [PATCH 5/9] add to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c8c348..85d4910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 that uses `Dual` and `HyperDual` directly (no derivator). @rtmongold (#106) - Fixed `examples/README.md` table entry for `optimization_solvers` (was an orphan bullet). @rtmongold (#117) +- `Primal` trait with `to_f64` and `to_f32` conversions. ### Fixed From f734b6d738e551d2cae7ee6376eadae8f5aab773 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 10:31:01 +0700 Subject: [PATCH 6/9] fix comments --- crates/multicalc/src/scalar/primal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/multicalc/src/scalar/primal.rs b/crates/multicalc/src/scalar/primal.rs index d5e8d49..59dfdb8 100644 --- a/crates/multicalc/src/scalar/primal.rs +++ b/crates/multicalc/src/scalar/primal.rs @@ -1,9 +1,9 @@ -//! One-way scalar-to-f64 projection for plotting. +//! One-way scalar-to-f64 and scalar-to-f32 projections. //! //! `multicalc::scalar::Numeric` provides `from_f64` but no `to_f64`, so this crate defines its //! own projection. It covers the float scalars and the autodiff scalars, projecting each to its //! primal (value) part so a differentiated quantity plots as its underlying value. The autodiff -//! impls delegate through the primal, so nested scalars such as `Dual>` work.use crate::{Dual, HyperDual, Jet, Numeric}; +//! impls delegate through the primal, so nested scalars such as `Dual>` work. use crate::{Dual, HyperDual, Jet, Numeric}; From 7ba63bb8d55ea3376adb3bf1bd8f0cd1f0016eb0 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 21:33:00 +0700 Subject: [PATCH 7/9] review fixes --- CHANGELOG.md | 2 +- crates/multicalc/src/scalar/mod.rs | 1 + crates/multicalc/src/scalar/primal.rs | 10 ++++++-- crates/multicalc/tests/scalar.rs | 33 +++++++++++++++------------ showcase/viz/src/lib.rs | 2 +- showcase/viz/src/sink.rs | 4 ++-- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d4910..8aa991b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 that uses `Dual` and `HyperDual` directly (no derivator). @rtmongold (#106) - Fixed `examples/README.md` table entry for `optimization_solvers` (was an orphan bullet). @rtmongold (#117) -- `Primal` trait with `to_f64` and `to_f32` conversions. +- `Primal` trait with `to_f64` and `to_f32` conversions. @ProtoFN (#131) ### Fixed diff --git a/crates/multicalc/src/scalar/mod.rs b/crates/multicalc/src/scalar/mod.rs index 9fc1f44..3fd09b9 100644 --- a/crates/multicalc/src/scalar/mod.rs +++ b/crates/multicalc/src/scalar/mod.rs @@ -16,3 +16,4 @@ pub use function::{ScalarFn, ScalarFnN, VectorFn, c}; pub use hyper_dual::HyperDual; pub use jet::Jet; pub use numeric::Numeric; +pub use primal::Primal; diff --git a/crates/multicalc/src/scalar/primal.rs b/crates/multicalc/src/scalar/primal.rs index 59dfdb8..685f0c5 100644 --- a/crates/multicalc/src/scalar/primal.rs +++ b/crates/multicalc/src/scalar/primal.rs @@ -1,13 +1,19 @@ //! One-way scalar-to-f64 and scalar-to-f32 projections. //! -//! `multicalc::scalar::Numeric` provides `from_f64` but no `to_f64`, so this crate defines its -//! own projection. It covers the float scalars and the autodiff scalars, projecting each to its +//! This module covers the float scalars and the autodiff scalars, projecting each to its //! primal (value) part so a differentiated quantity plots as its underlying value. The autodiff //! impls delegate through the primal, so nested scalars such as `Dual>` work. use crate::{Dual, HyperDual, Jet, Numeric}; /// A scalar that can be projected to `f64` and `f32`. +/// +/// ``` +/// use multicalc::scalar::{Dual, Primal}; +/// +/// let x = Dual::new(2.0, 99.0); +/// assert_eq!(x.to_f64(), 2.0); +/// ``` pub trait Primal { /// Returns the value as an `f64` (the primal part, for autodiff scalars). fn to_f64(&self) -> f64; diff --git a/crates/multicalc/tests/scalar.rs b/crates/multicalc/tests/scalar.rs index 2dcd186..e6cd357 100644 --- a/crates/multicalc/tests/scalar.rs +++ b/crates/multicalc/tests/scalar.rs @@ -78,11 +78,11 @@ mod numeric_methods { } mod primal { - use multicalc::{Dual, HyperDual, Jet, Numeric, scalar::primal::Primal}; + use multicalc::{Dual, HyperDual, Jet, scalar::Primal}; #[test] fn test_f64() { - let two = f64::TWO; + let two = 2.0; assert_eq!(two.to_f64(), two); assert_eq!(two.to_f32(), two as f32); @@ -90,7 +90,7 @@ mod primal { #[test] fn test_f32() { - let two = f32::TWO; + let two = 2.0; assert_eq!(two.to_f64(), two as f64); assert_eq!(two.to_f32(), two); @@ -98,34 +98,37 @@ mod primal { #[test] fn test_dual() { - let two = Dual::::TWO; + let two = Dual::new(2.0, 99.0); - assert_eq!(two.to_f64(), f64::TWO); - assert_eq!(two.to_f32(), f32::TWO); + assert_eq!(two.to_f64(), 2.0); + assert_eq!(two.to_f32(), 2.0); } #[test] fn test_hyperdual() { - let two = HyperDual::::TWO; + let two = HyperDual::new(2.0, 10.0, 20.0, 1020.0); - assert_eq!(two.to_f64(), f64::TWO); - assert_eq!(two.to_f32(), f32::TWO); + assert_eq!(two.to_f64(), 2.0); + assert_eq!(two.to_f32(), 2.0); } #[test] fn test_jet() { - let two = Jet::::TWO; + let two = Jet::new([2.0, 99.0]); - assert_eq!(two.to_f64(), f64::TWO); - assert_eq!(two.to_f32(), f32::TWO); + assert_eq!(two.to_f64(), 2.0); + assert_eq!(two.to_f32(), 2.0); } #[test] fn test_dual_hyperdual() { - let two = Dual::>::TWO; + let two = Dual::new( + HyperDual::new(2.0, 10.0, 20.0, 1020.0), + HyperDual::new(6.0, 30.0, 60.0, 3060.0), + ); - assert_eq!(two.to_f64(), f64::TWO); - assert_eq!(two.to_f32(), f32::TWO); + assert_eq!(two.to_f64(), 2.0); + assert_eq!(two.to_f32(), 2.0); } } diff --git a/showcase/viz/src/lib.rs b/showcase/viz/src/lib.rs index 2b48c61..00baf4b 100644 --- a/showcase/viz/src/lib.rs +++ b/showcase/viz/src/lib.rs @@ -12,7 +12,7 @@ mod sink; pub mod loop_util; pub use csv_sink::CsvSink; -pub use multicalc::scalar::primal::Primal; +pub use multicalc::scalar::Primal; pub use rerun_sink::RerunSink; pub use sink::{Rgba, VizError, VizSink, VizSinkExt}; diff --git a/showcase/viz/src/sink.rs b/showcase/viz/src/sink.rs index 2c3f98a..6eb7ead 100644 --- a/showcase/viz/src/sink.rs +++ b/showcase/viz/src/sink.rs @@ -2,9 +2,9 @@ //! //! [`VizSink`] is kept object-safe so callers can hold a `&mut dyn VizSink` and swap backends at //! runtime; that is why `scalar` takes a plain `f64`. The generic convenience form that accepts -//! any [`Plottable`] scalar lives on the blanket [`VizSinkExt`]. +//! any [`Primal`] scalar lives on the blanket [`VizSinkExt`]. -use multicalc::scalar::primal::Primal; +use multicalc::scalar::Primal; use core::fmt; From 6ccb0601be56b8b6bc3aaf7dddf1bf211f025451 Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 23:13:05 +0700 Subject: [PATCH 8/9] bureaucracy fixes --- CHANGELOG.md | 3 ++- crates/multicalc/src/scalar/primal.rs | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aa991b..546811b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 that uses `Dual` and `HyperDual` directly (no derivator). @rtmongold (#106) - Fixed `examples/README.md` table entry for `optimization_solvers` (was an orphan bullet). @rtmongold (#117) -- `Primal` trait with `to_f64` and `to_f32` conversions. @ProtoFN (#131) +- **`Primal` scalar projection.** New `to_f64` / `to_f32` trait for edge use (viz, logging, etc.), kept separate from + `Numeric`. `showcase/viz` now uses it instead of its private `Plottable` trait. @ProtoFN (#131) ### Fixed diff --git a/crates/multicalc/src/scalar/primal.rs b/crates/multicalc/src/scalar/primal.rs index 685f0c5..cee2b0f 100644 --- a/crates/multicalc/src/scalar/primal.rs +++ b/crates/multicalc/src/scalar/primal.rs @@ -1,8 +1,7 @@ -//! One-way scalar-to-f64 and scalar-to-f32 projections. +//! Edge-only projection to `f64` / `f32` (the reverse of [`Numeric::from_f64`]). //! -//! This module covers the float scalars and the autodiff scalars, projecting each to its -//! primal (value) part so a differentiated quantity plots as its underlying value. The autodiff -//! impls delegate through the primal, so nested scalars such as `Dual>` work. +//! Kept separate from [`Numeric`] so generic algorithms cannot strip autodiff. +//! Autodiff types return their value part; nesting such as `Dual>` works. use crate::{Dual, HyperDual, Jet, Numeric}; From 00d702ed2f8160afdc3aefd8624f9bbf1d34dfaf Mon Sep 17 00:00:00 2001 From: FN Date: Sun, 12 Jul 2026 23:52:20 +0700 Subject: [PATCH 9/9] review fix --- crates/multicalc/tests/scalar.rs | 59 +++++++------------------------- 1 file changed, 13 insertions(+), 46 deletions(-) diff --git a/crates/multicalc/tests/scalar.rs b/crates/multicalc/tests/scalar.rs index e6cd357..ab3a5cd 100644 --- a/crates/multicalc/tests/scalar.rs +++ b/crates/multicalc/tests/scalar.rs @@ -78,57 +78,24 @@ mod numeric_methods { } mod primal { - use multicalc::{Dual, HyperDual, Jet, scalar::Primal}; + use multicalc::scalar::Primal; + use multicalc::{Dual, HyperDual, Jet}; #[test] - fn test_f64() { - let two = 2.0; + fn projects_floats_and_autodiff_primals() { + assert_eq!(2.0_f64.to_f64(), 2.0); + assert_eq!(2.0_f32.to_f64(), 2.0); - assert_eq!(two.to_f64(), two); - assert_eq!(two.to_f32(), two as f32); - } - - #[test] - fn test_f32() { - let two = 2.0; - - assert_eq!(two.to_f64(), two as f64); - assert_eq!(two.to_f32(), two); - } + assert_eq!(Dual::new(2.0, 99.0).to_f64(), 2.0); + assert_eq!(HyperDual::new(2.0, 1.0, 1.0, 1.0).to_f64(), 2.0); + assert_eq!(Jet::::variable(2.0).to_f64(), 2.0); - #[test] - fn test_dual() { - let two = Dual::new(2.0, 99.0); - - assert_eq!(two.to_f64(), 2.0); - assert_eq!(two.to_f32(), 2.0); - } - - #[test] - fn test_hyperdual() { - let two = HyperDual::new(2.0, 10.0, 20.0, 1020.0); - - assert_eq!(two.to_f64(), 2.0); - assert_eq!(two.to_f32(), 2.0); - } - - #[test] - fn test_jet() { - let two = Jet::new([2.0, 99.0]); - - assert_eq!(two.to_f64(), 2.0); - assert_eq!(two.to_f32(), 2.0); - } - - #[test] - fn test_dual_hyperdual() { - let two = Dual::new( - HyperDual::new(2.0, 10.0, 20.0, 1020.0), - HyperDual::new(6.0, 30.0, 60.0, 3060.0), + let nested = Dual::new( + HyperDual::new(2.0, 1.0, 1.0, 1.0), + HyperDual::new(99.0, 3.0, 3.0, 3.0), ); - - assert_eq!(two.to_f64(), 2.0); - assert_eq!(two.to_f32(), 2.0); + assert_eq!(nested.to_f64(), 2.0); + assert_eq!(nested.to_f32(), 2.0); } }