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}