diff --git a/CHANGELOG.md b/CHANGELOG.md index aa3641c..c668999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +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` 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) - Property tests for linear/quadratic approximation invariants (exactness on matching-degree polynomials, metrics consistency). @rtmongold (#130) - Property tests that autodiff and central finite-difference derivatives agree on random diff --git a/crates/multicalc/src/scalar/mod.rs b/crates/multicalc/src/scalar/mod.rs index 535a31a..3fd09b9 100644 --- a/crates/multicalc/src/scalar/mod.rs +++ b/crates/multicalc/src/scalar/mod.rs @@ -9,9 +9,11 @@ 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}; 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 new file mode 100644 index 0000000..cee2b0f --- /dev/null +++ b/crates/multicalc/src/scalar/primal.rs @@ -0,0 +1,72 @@ +//! Edge-only projection to `f64` / `f32` (the reverse of [`Numeric::from_f64`]). +//! +//! 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}; + +/// 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; + + /// Returns the value as an `f32` (the primal part, for autodiff scalars). + 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() + } +} diff --git a/crates/multicalc/tests/scalar.rs b/crates/multicalc/tests/scalar.rs index b97fa7b..ab3a5cd 100644 --- a/crates/multicalc/tests/scalar.rs +++ b/crates/multicalc/tests/scalar.rs @@ -77,6 +77,28 @@ mod numeric_methods { } } +mod primal { + use multicalc::scalar::Primal; + use multicalc::{Dual, HyperDual, Jet}; + + #[test] + 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!(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); + + 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!(nested.to_f64(), 2.0); + assert_eq!(nested.to_f32(), 2.0); + } +} + mod dual { use multicalc::scalar::Dual; use multicalc::scalar::Numeric; 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..00baf4b 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; 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..6eb7ead 100644 --- a/showcase/viz/src/sink.rs +++ b/showcase/viz/src/sink.rs @@ -2,9 +2,10 @@ //! //! [`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; -use crate::convert::Plottable; 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()) } }