Skip to main content

scirs2_integrate/symbolic/
conversion.rs

1//! Higher-order ODE to first-order system conversion
2//!
3//! This module provides functionality to automatically convert higher-order
4//! ODEs into systems of first-order ODEs, which is required by most
5//! numerical integration methods.
6
7use super::expression::{SymbolicExpression, Variable};
8use crate::common::IntegrateFloat;
9use crate::error::{IntegrateError, IntegrateResult};
10use scirs2_core::ndarray::{Array1, ArrayView1};
11use std::collections::HashMap;
12use SymbolicExpression::{Add, Constant, Cos, Div, Exp, Ln, Mul, Neg, Pow, Sin, Sqrt, Sub, Var};
13
14/// Represents a higher-order ODE
15pub struct HigherOrderODE<F: IntegrateFloat> {
16    /// The order of the ODE
17    pub order: usize,
18    /// The dependent variable name
19    pub dependent_var: String,
20    /// The independent variable name (usually time)
21    pub independent_var: String,
22    /// The symbolic expression for the highest derivative
23    /// as a function of lower derivatives and the independent variable
24    pub expression: SymbolicExpression<F>,
25}
26
27impl<F: IntegrateFloat> HigherOrderODE<F> {
28    /// Create a new higher-order ODE
29    pub fn new(
30        order: usize,
31        dependent_var: impl Into<String>,
32        independent_var: impl Into<String>,
33        expression: SymbolicExpression<F>,
34    ) -> IntegrateResult<Self> {
35        if order == 0 {
36            return Err(IntegrateError::ValueError(
37                "ODE order must be at least 1".to_string(),
38            ));
39        }
40
41        Ok(HigherOrderODE {
42            order,
43            dependent_var: dependent_var.into(),
44            independent_var: independent_var.into(),
45            expression,
46        })
47    }
48
49    /// Get the state variable names for the first-order system
50    pub fn state_variables(&self) -> Vec<Variable> {
51        (0..self.order)
52            .map(|i| Variable::indexed(&self.dependent_var, i))
53            .collect()
54    }
55}
56
57/// Result of converting a higher-order ODE to first-order system
58pub struct FirstOrderSystem<F: IntegrateFloat> {
59    /// The state variables (`y[0]` = x, `y[1]` = x', `y[2]` = x'', etc.)
60    pub state_vars: Vec<Variable>,
61    /// The expressions for dy/dt
62    pub expressions: Vec<SymbolicExpression<F>>,
63    /// Mapping from derivative notation to state variables
64    pub variable_map: HashMap<String, Variable>,
65}
66
67impl<F: IntegrateFloat> FirstOrderSystem<F> {
68    /// Convert to a function suitable for ODE solvers
69    pub fn to_function(&self) -> impl Fn(F, ArrayView1<F>) -> IntegrateResult<Array1<F>> {
70        let expressions = self.expressions.clone();
71        let state_vars = self.state_vars.clone();
72
73        move |t: F, y: ArrayView1<F>| {
74            if y.len() != state_vars.len() {
75                return Err(IntegrateError::DimensionMismatch(format!(
76                    "Expected {} states, got {}",
77                    state_vars.len(),
78                    y.len()
79                )));
80            }
81
82            // Build value map
83            let mut values = HashMap::new();
84            for (i, var) in state_vars.iter().enumerate() {
85                values.insert(var.clone(), y[i]);
86            }
87            values.insert(Variable::new("t"), t);
88
89            // Evaluate expressions
90            let mut result = Array1::zeros(expressions.len());
91            for (i, expr) in expressions.iter().enumerate() {
92                result[i] = expr.evaluate(&values)?;
93            }
94
95            Ok(result)
96        }
97    }
98}
99
100/// Convert a higher-order ODE to a first-order system
101///
102/// # Arguments
103/// * `ode` - The higher-order ODE to convert
104///
105/// # Returns
106/// A first-order system equivalent to the input ODE
107///
108/// # Example
109/// For a second-order ODE: x'' = -x - 0.1*x'
110/// This converts to:
111/// - `y[0]` = x
112/// - `y[1]` = x'
113/// - `dy[0]/dt` = `y[1]`
114/// - `dy[1]/dt` = `-y[0]` - 0.1*`y[1]`
115#[allow(dead_code)]
116pub fn higher_order_to_first_order<F: IntegrateFloat>(
117    ode: &HigherOrderODE<F>,
118) -> IntegrateResult<FirstOrderSystem<F>> {
119    use SymbolicExpression::*;
120
121    let mut state_vars = Vec::new();
122    let mut expressions = Vec::new();
123    let mut variable_map = HashMap::new();
124
125    // Create state variables for each derivative
126    for i in 0..ode.order {
127        let var = Variable::indexed(&ode.dependent_var, i);
128        state_vars.push(var.clone());
129
130        // Map derivative notation to state variable
131        let deriv_notation = match i {
132            0 => ode.dependent_var.clone(),
133            1 => format!("{}'", ode.dependent_var),
134            n => format!("{}^({})", ode.dependent_var, n),
135        };
136        variable_map.insert(deriv_notation, var);
137    }
138
139    // Create expressions for the first-order system
140    // dy[i]/dt = y[i+1] for i < order-1
141    for i in 0..ode.order - 1 {
142        expressions.push(Var(state_vars[i + 1].clone()));
143    }
144
145    // For the highest derivative, substitute state variables
146    let mut highest_deriv_expr = ode.expression.clone();
147    highest_deriv_expr = substitute_derivatives(&highest_deriv_expr, &variable_map);
148    expressions.push(highest_deriv_expr);
149
150    Ok(FirstOrderSystem {
151        state_vars,
152        expressions,
153        variable_map,
154    })
155}
156
157/// Substitute derivative notation with state variables in an expression
158#[allow(dead_code)]
159fn substitute_derivatives<F: IntegrateFloat>(
160    expr: &SymbolicExpression<F>,
161    variable_map: &HashMap<String, Variable>,
162) -> SymbolicExpression<F> {
163    match expr {
164        Var(v) => {
165            // Check if this variable should be substituted
166            if let Some(state_var) = variable_map.get(&v.name) {
167                Var(state_var.clone())
168            } else {
169                expr.clone()
170            }
171        }
172        Add(a, b) => Add(
173            Box::new(substitute_derivatives(a, variable_map)),
174            Box::new(substitute_derivatives(b, variable_map)),
175        ),
176        Sub(a, b) => Sub(
177            Box::new(substitute_derivatives(a, variable_map)),
178            Box::new(substitute_derivatives(b, variable_map)),
179        ),
180        Mul(a, b) => Mul(
181            Box::new(substitute_derivatives(a, variable_map)),
182            Box::new(substitute_derivatives(b, variable_map)),
183        ),
184        Div(a, b) => Div(
185            Box::new(substitute_derivatives(a, variable_map)),
186            Box::new(substitute_derivatives(b, variable_map)),
187        ),
188        Pow(a, b) => Pow(
189            Box::new(substitute_derivatives(a, variable_map)),
190            Box::new(substitute_derivatives(b, variable_map)),
191        ),
192        Neg(a) => Neg(Box::new(substitute_derivatives(a, variable_map))),
193        Sin(a) => Sin(Box::new(substitute_derivatives(a, variable_map))),
194        Cos(a) => Cos(Box::new(substitute_derivatives(a, variable_map))),
195        Exp(a) => Exp(Box::new(substitute_derivatives(a, variable_map))),
196        Ln(a) => Ln(Box::new(substitute_derivatives(a, variable_map))),
197        Sqrt(a) => Sqrt(Box::new(substitute_derivatives(a, variable_map))),
198        _ => expr.clone(),
199    }
200}
201
202/// Example: Convert a damped harmonic oscillator to first-order system
203#[allow(dead_code)]
204pub fn example_damped_oscillator<F: IntegrateFloat>(
205    omega: F,
206    damping: F,
207) -> IntegrateResult<FirstOrderSystem<F>> {
208    // Second-order ODE: x'' + 2*damping*x' + omega^2*x = 0
209    // Rearranged: x'' = -2*damping*x' - omega^2*x
210
211    let x = Var(Variable::new("x"));
212    let x_prime = Var(Variable::new("x'"));
213
214    let expression = Neg(Box::new(Add(
215        Box::new(Mul(
216            Box::new(Mul(
217                Box::new(Constant(
218                    F::from(2.0).expect("Failed to convert constant to float"),
219                )),
220                Box::new(Constant(damping)),
221            )),
222            Box::new(x_prime),
223        )),
224        Box::new(Mul(
225            Box::new(Pow(
226                Box::new(Constant(omega)),
227                Box::new(Constant(
228                    F::from(2.0).expect("Failed to convert constant to float"),
229                )),
230            )),
231            Box::new(x),
232        )),
233    )));
234
235    let ode = HigherOrderODE::new(2, "x", "t", expression)?;
236    higher_order_to_first_order(&ode)
237}
238
239/// Example: Convert a driven pendulum equation to first-order system
240#[allow(dead_code)]
241pub fn example_driven_pendulum<F: IntegrateFloat>(
242    g: F,     // gravity
243    l: F,     // length
244    gamma: F, // damping coefficient
245    a: F,     // driving amplitude
246    omega: F, // driving frequency
247) -> IntegrateResult<FirstOrderSystem<F>> {
248    // Pendulum equation: θ'' + (g/l)*sin(θ) + γ*θ' = A*cos(ω*t)
249    // Rearranged: θ'' = -γ*θ' - (g/l)*sin(θ) + A*cos(ω*t)
250
251    let theta = SymbolicExpression::var("θ");
252    let theta_prime = SymbolicExpression::var("θ'");
253    let t = SymbolicExpression::var("t");
254
255    let g_over_l = SymbolicExpression::constant(g / l);
256    let gamma_const = SymbolicExpression::constant(gamma);
257    let a_const = SymbolicExpression::constant(a);
258    let omega_const = SymbolicExpression::constant(omega);
259
260    // Using operator overloading
261    let damping_term = -gamma_const * theta_prime;
262    let gravity_term = -g_over_l * SymbolicExpression::Sin(Box::new(theta));
263    let driving_term = a_const * SymbolicExpression::Cos(Box::new(omega_const * t));
264
265    let expression = damping_term + gravity_term + driving_term;
266
267    let ode = HigherOrderODE::new(2, "θ", "t", expression)?;
268    higher_order_to_first_order(&ode)
269}
270
271/// Example: Convert a beam equation (4th order) to first-order system
272#[allow(dead_code)]
273pub fn example_euler_bernoulli_beam<F: IntegrateFloat>(
274    ei: F,     // flexural rigidity
275    _rho_a: F, // mass per unit length
276    f: F,      // distributed load
277) -> IntegrateResult<FirstOrderSystem<F>> {
278    // Euler-Bernoulli beam equation: EI*w'''' + ρA*∂²w/∂t² = f(x,t)
279    // For static case: EI*w'''' = f(x)
280    // Rearranged: w'''' = f/(EI)
281
282    let f_over_ei = SymbolicExpression::constant(f / ei);
283
284    let ode = HigherOrderODE::new(4, "w", "x", f_over_ei)?;
285    higher_order_to_first_order(&ode)
286}
287
288/// Convert a system of higher-order ODEs to first-order
289pub struct SystemConverter<F: IntegrateFloat> {
290    odes: Vec<HigherOrderODE<F>>,
291    total_states: usize,
292}
293
294impl<F: IntegrateFloat> SystemConverter<F> {
295    /// Create a new system converter
296    pub fn new() -> Self {
297        SystemConverter {
298            odes: Vec::new(),
299            total_states: 0,
300        }
301    }
302
303    /// Add a higher-order ODE to the system
304    pub fn add_ode(&mut self, ode: HigherOrderODE<F>) -> &mut Self {
305        self.total_states += ode.order;
306        self.odes.push(ode);
307        self
308    }
309
310    /// Convert the entire system to first-order
311    pub fn convert(&self) -> IntegrateResult<FirstOrderSystem<F>> {
312        let mut all_state_vars = Vec::new();
313        let mut all_expressions = Vec::new();
314        let mut all_variable_map = HashMap::new();
315
316        for ode in &self.odes {
317            let system = higher_order_to_first_order(ode)?;
318            all_state_vars.extend(system.state_vars);
319            all_expressions.extend(system.expressions);
320            all_variable_map.extend(system.variable_map);
321        }
322
323        Ok(FirstOrderSystem {
324            state_vars: all_state_vars,
325            expressions: all_expressions,
326            variable_map: all_variable_map,
327        })
328    }
329}
330
331impl<F: IntegrateFloat> Default for SystemConverter<F> {
332    fn default() -> Self {
333        Self::new()
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::{
341        higher_order_to_first_order, HigherOrderODE, SymbolicExpression,
342        SymbolicExpression::{Neg, Var},
343        Variable,
344    };
345
346    #[test]
347    fn test_second_order_conversion() {
348        // Test x'' = -x
349        let x: SymbolicExpression<f64> = Var(Variable::new("x"));
350        let expr = Neg(Box::new(x));
351
352        let ode = HigherOrderODE::new(2, "x", "t", expr).expect("Operation failed");
353        let system = higher_order_to_first_order(&ode).expect("Operation failed");
354
355        assert_eq!(system.state_vars.len(), 2);
356        assert_eq!(system.expressions.len(), 2);
357
358        // Check that dy[0]/dt = y[1]
359        if let Var(v) = &system.expressions[0] {
360            assert_eq!(v.name, "x");
361            assert_eq!(v.index, Some(1));
362        } else {
363            panic!(
364                "Expected variable expression, got {:?}",
365                system.expressions[0]
366            );
367        }
368    }
369}