twine_core/
time.rs

1use std::{
2    fmt::Debug,
3    ops::{Add, Div, Mul},
4    time::Duration,
5};
6
7use uom::si::{f64::Time, time::second};
8
9/// A trait for types that support finite time stepping via integration.
10///
11/// Types that implement this trait must have a well-defined time derivative and
12/// be able to advance their state over a specified time interval.
13///
14/// This trait is primarily intended for unit-aware physical quantities,
15/// such as those provided by the [`uom`] crate.
16///
17/// It imposes no operator bounds directly. However, it is automatically
18/// implemented for any type `T` that satisfies the following bounds:
19///
20/// - `T: Div<Time, Output = Derivative>`: Defines the derivative as `T / Time`
21/// - `Derivative: Mul<Time, Output = Delta>`: Defines the delta as `Derivative * Time`
22/// - `T: Add<Delta, Output = T>`: Enables applying a delta to produce an updated value
23/// - `T` and `Derivative` implement `Debug`, `Clone`, and `PartialEq`
24///
25/// For such types, which include all `uom` quantities,
26/// integration is performed using a forward Euler step:
27///
28/// ```text
29/// next_value = self + derivative * dt
30/// ```
31///
32/// # Example
33///
34/// To implement [`TimeIntegrable`] manually for a composite type,
35/// define a corresponding derivative type and delegate the integration
36/// logic to each field:
37///
38/// ```
39/// use twine_core::{TimeIntegrable, TimeDerivative};
40/// use uom::si::f64::{MassDensity, ThermodynamicTemperature, Time};
41///
42/// #[derive(Debug, Clone, PartialEq)]
43/// struct State<T: TimeIntegrable> {
44///     temperature: ThermodynamicTemperature,
45///     density: MassDensity,
46///     other: T,
47/// }
48///
49/// #[derive(Debug, Clone, PartialEq)]
50/// struct StateTimeDerivative<T: TimeIntegrable> {
51///     temperature: TimeDerivative<ThermodynamicTemperature>,
52///     density: TimeDerivative<MassDensity>,
53///     other: TimeDerivative<T>,
54/// }
55///
56/// impl<T: TimeIntegrable> TimeIntegrable for State<T> {
57///     type Derivative = StateTimeDerivative<T>;
58///
59///     fn step(self, derivative: Self::Derivative, dt: Time) -> Self {
60///         Self {
61///             temperature: self.temperature.step(derivative.temperature, dt),
62///             density: self.density.step(derivative.density, dt),
63///             other: self.other.step(derivative.other, dt),
64///         }
65///     }
66/// }
67/// ```
68///
69/// Alternatively, you can derive this implementation automatically using the
70/// `#[derive(TimeIntegrable)]` macro from the [`twine_macros`] crate:
71///
72/// ```ignore
73/// use twine_macros::TimeIntegrable;
74/// use uom::si::f64::{MassDensity, ThermodynamicTemperature};
75///
76/// #[derive(Debug, Clone, PartialEq, TimeIntegrable)]
77/// struct State {
78///     temperature: ThermodynamicTemperature,
79///     density: MassDensity,
80/// }
81/// ```
82///
83/// This generates the same `StateTimeDerivative` struct and [`TimeIntegrable`]
84/// implementation as shown above.
85pub trait TimeIntegrable: Debug + Clone + PartialEq {
86    type Derivative: Debug + Clone + PartialEq;
87
88    /// Advances the value using its derivative over a time interval.
89    #[must_use]
90    fn step(self, derivative: Self::Derivative, dt: Time) -> Self;
91}
92
93impl<T, Derivative, Delta> TimeIntegrable for T
94where
95    T: Debug + Clone + PartialEq,
96    T: Div<Time, Output = Derivative> + Add<Delta, Output = T>,
97    Derivative: Debug + Clone + PartialEq,
98    Derivative: Mul<Time, Output = Delta>,
99{
100    type Derivative = Derivative;
101
102    /// Computes a forward Euler integration step.
103    fn step(self, derivative: Self::Derivative, dt: Time) -> Self {
104        self + derivative * dt
105    }
106}
107
108/// The time derivative associated with a `TimeIntegrable` type `T`.
109///
110/// This alias is useful in type-level contexts (e.g., struct fields that
111/// represent time derivatives), especially when working with unit-aware types
112/// from the `uom` crate.
113///
114/// # Examples
115///
116/// - `TimeDerivative<Length>` = `Velocity`
117/// - `TimeDerivative<Velocity>` = `Acceleration`
118pub type TimeDerivative<T> = <T as TimeIntegrable>::Derivative;
119
120/// Extension trait for ergonomic operations on [`Duration`].
121///
122/// This trait provides additional utilities for working with [`std::time::Duration`],
123/// such as unit-aware conversions and other common operations involving time.
124///
125/// While it currently defines only a single method, it is expected to grow into
126/// a collection of [`Duration`]-related functionality as the need arises.
127///
128/// # Example
129///
130/// ```
131/// use std::time::Duration;
132///
133/// use twine_core::DurationExt;
134/// use uom::si::{f64::Time, time::second};
135///
136/// let dt = Duration::from_secs_f64(2.5);
137/// let t: Time = dt.as_time();
138///
139/// assert_eq!(t.get::<second>(), 2.5);
140/// ```
141pub trait DurationExt {
142    /// Converts this [`Duration`] into a [`uom::si::f64::Time`] quantity.
143    fn as_time(&self) -> Time;
144}
145
146impl DurationExt for Duration {
147    fn as_time(&self) -> Time {
148        Time::new::<second>(self.as_secs_f64())
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    use approx::assert_relative_eq;
157    use uom::si::{
158        f64::{Length, TemperatureInterval, ThermodynamicTemperature, Time, Velocity},
159        length::meter,
160        temperature_interval::degree_celsius,
161        thermodynamic_temperature::kelvin,
162        time::{minute, second},
163        velocity::meter_per_second,
164    };
165
166    #[test]
167    fn step_length_forward() {
168        let position = Length::new::<meter>(5.0);
169        let velocity = Velocity::new::<meter_per_second>(2.0);
170        let dt = Time::new::<second>(1.5);
171
172        let next_position = position.step(velocity, dt);
173        assert_relative_eq!(next_position.get::<meter>(), 8.0);
174    }
175
176    #[test]
177    fn step_temperature_forward() {
178        let temperature = ThermodynamicTemperature::new::<kelvin>(300.0);
179        let rate = TemperatureInterval::new::<degree_celsius>(10.0) / Time::new::<minute>(1.0);
180        let dt = Time::new::<second>(30.0);
181
182        let next_temperature = temperature.step(rate, dt);
183        assert_relative_eq!(next_temperature.get::<kelvin>(), 305.0);
184    }
185}