Skip to main content

alkahest_cas/acausal/
mod.rs

1//! Phase 18 — Acausal component modelling.
2//!
3//! Models physical systems as networks of *components* connected through
4//! *ports*.  Each port has a *potential* (across) variable and a *flow*
5//! (through) variable.  Kirchhoff-style connection equations enforce:
6//!
7//! - **Potential continuity**: potentials at connected ports are equal.
8//! - **Flow balance**: flows at connected ports sum to zero (Kirchhoff's
9//!   current law / Newton's third law for force).
10//!
11//! After all components and connections are specified, `flatten` collects all
12//! equations into a single `DAE` ready for the Pantelides analyser (Phase 17).
13//!
14//! # Example (resistor-capacitor circuit)
15//!
16//! ```text
17//! Resistor: v_R = R * i_R
18//! Capacitor: C * dv_C/dt = i_C
19//! connect(Resistor.pin_p, Capacitor.pin_p)  →  v_R = v_C, i_R + i_C = 0
20//! ```
21
22use crate::dae::DAE;
23use crate::kernel::{Domain, ExprId, ExprPool};
24
25// ---------------------------------------------------------------------------
26// Port
27// ---------------------------------------------------------------------------
28
29/// A connection port with a *potential* (across) and a *flow* (through) variable.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Port {
32    /// Name of this port (for diagnostics)
33    pub name: String,
34    /// Potential (voltage, pressure, temperature, …)
35    pub potential: ExprId,
36    /// Flow (current, mass flow, heat flux, …)
37    pub flow: ExprId,
38}
39
40impl Port {
41    /// Create a new port, allocating fresh symbolic variables in `pool`.
42    pub fn new(name: &str, pool: &ExprPool) -> Self {
43        let potential = pool.symbol(format!("{name}.v"), Domain::Real);
44        let flow = pool.symbol(format!("{name}.i"), Domain::Real);
45        Port {
46            name: name.to_string(),
47            potential,
48            flow,
49        }
50    }
51}
52
53// ---------------------------------------------------------------------------
54// Component
55// ---------------------------------------------------------------------------
56
57/// A physical component with named ports and internal constitutive equations.
58#[derive(Clone, Debug)]
59pub struct Component {
60    /// Component name (for diagnostics)
61    pub name: String,
62    /// External connection ports
63    pub ports: Vec<Port>,
64    /// Internal (constitutive) equations in the form `expr = 0`
65    pub equations: Vec<ExprId>,
66    /// Internal variables not exposed as ports
67    pub internal_vars: Vec<ExprId>,
68    /// Derivative symbols for internal differential variables
69    pub internal_derivs: Vec<ExprId>,
70}
71
72impl Component {
73    /// Create a new empty component.
74    pub fn new(name: &str) -> Self {
75        Component {
76            name: name.to_string(),
77            ports: vec![],
78            equations: vec![],
79            internal_vars: vec![],
80            internal_derivs: vec![],
81        }
82    }
83
84    /// Add a port to this component.
85    pub fn add_port(mut self, port: Port) -> Self {
86        self.ports.push(port);
87        self
88    }
89
90    /// Add a constitutive equation (implicit form `expr = 0`).
91    pub fn add_equation(mut self, eq: ExprId) -> Self {
92        self.equations.push(eq);
93        self
94    }
95
96    /// Add an internal variable with its derivative symbol.
97    pub fn add_internal_var(mut self, var: ExprId, deriv: ExprId) -> Self {
98        self.internal_vars.push(var);
99        self.internal_derivs.push(deriv);
100        self
101    }
102
103    /// Get a port by name.
104    pub fn port(&self, name: &str) -> Option<&Port> {
105        self.ports.iter().find(|p| p.name == name)
106    }
107}
108
109// ---------------------------------------------------------------------------
110// System builder
111// ---------------------------------------------------------------------------
112
113/// A connection between two ports (equality of potentials, zero-sum of flows).
114#[derive(Clone, Debug)]
115struct Connection {
116    port_a: Port,
117    port_b: Port,
118}
119
120/// A network of components and their connections.
121#[derive(Clone, Debug, Default)]
122pub struct System {
123    components: Vec<Component>,
124    connections: Vec<Connection>,
125}
126
127impl System {
128    pub fn new() -> Self {
129        System::default()
130    }
131
132    /// Add a component to the system.
133    pub fn add_component(&mut self, component: Component) {
134        self.components.push(component);
135    }
136
137    /// Connect port `a` to port `b`.
138    ///
139    /// This adds two connection equations:
140    /// - `a.potential - b.potential = 0`
141    /// - `a.flow + b.flow = 0`
142    pub fn connect(&mut self, port_a: &Port, port_b: &Port) {
143        self.connections.push(Connection {
144            port_a: port_a.clone(),
145            port_b: port_b.clone(),
146        });
147    }
148
149    /// Flatten all component equations and connection equations into a single `DAE`.
150    ///
151    /// All port and internal variables become DAE variables.
152    /// Derivative symbols are generated with the `d{name}/dt` convention.
153    pub fn flatten(&self, time_var: ExprId, pool: &ExprPool) -> DAE {
154        let mut equations: Vec<ExprId> = Vec::new();
155        let mut variables: Vec<ExprId> = Vec::new();
156        let mut derivatives: Vec<ExprId> = Vec::new();
157
158        // Collect all component equations and variables
159        for comp in &self.components {
160            equations.extend_from_slice(&comp.equations);
161
162            // Port variables
163            for port in &comp.ports {
164                add_var_if_new(port.potential, pool, &mut variables, &mut derivatives);
165                add_var_if_new(port.flow, pool, &mut variables, &mut derivatives);
166            }
167
168            // Internal variables (with their explicit derivative symbols)
169            for (&var, &deriv) in comp.internal_vars.iter().zip(comp.internal_derivs.iter()) {
170                if !variables.contains(&var) {
171                    variables.push(var);
172                    derivatives.push(deriv);
173                }
174            }
175        }
176
177        // Generate connection equations
178        let neg_one = pool.integer(-1_i32);
179        for conn in &self.connections {
180            // potential_a - potential_b = 0
181            let neg_b = pool.mul(vec![neg_one, conn.port_b.potential]);
182            let pot_eq = pool.add(vec![conn.port_a.potential, neg_b]);
183            equations.push(pot_eq);
184
185            // flow_a + flow_b = 0
186            let flow_eq = pool.add(vec![conn.port_a.flow, conn.port_b.flow]);
187            equations.push(flow_eq);
188        }
189
190        DAE::new(equations, variables, derivatives, time_var)
191    }
192}
193
194/// Add `var` to `variables` if not already present, creating a derivative symbol.
195fn add_var_if_new(
196    var: ExprId,
197    pool: &ExprPool,
198    variables: &mut Vec<ExprId>,
199    derivatives: &mut Vec<ExprId>,
200) {
201    if variables.contains(&var) {
202        return;
203    }
204    let deriv_name = pool.with(var, |d| match d {
205        crate::kernel::ExprData::Symbol { name, .. } => format!("d{name}/dt"),
206        _ => "d?/dt".to_string(),
207    });
208    let deriv = pool.symbol(&deriv_name, Domain::Real);
209    variables.push(var);
210    derivatives.push(deriv);
211}
212
213// ---------------------------------------------------------------------------
214// Convenience constructors for standard components
215// ---------------------------------------------------------------------------
216
217/// Create a linear resistor: `v - R*i = 0`
218pub fn resistor(name: &str, resistance: ExprId, pool: &ExprPool) -> Component {
219    let port_p = Port::new(&format!("{name}.p"), pool);
220    let port_n = Port::new(&format!("{name}.n"), pool);
221
222    // Voltage across: v = port_p.v - port_n.v
223    let v = pool.add(vec![
224        port_p.potential,
225        pool.mul(vec![pool.integer(-1_i32), port_n.potential]),
226    ]);
227    // Current: i = port_p.i  (positive into component)
228    let i = port_p.flow;
229
230    // Ohm's law: v - R*i = 0
231    let ri = pool.mul(vec![resistance, i]);
232    let eq = pool.add(vec![v, pool.mul(vec![pool.integer(-1_i32), ri])]);
233
234    Component::new(name)
235        .add_port(port_p)
236        .add_port(port_n)
237        .add_equation(eq)
238}
239
240/// Create a capacitor: `C * dv/dt - i = 0`
241pub fn capacitor(name: &str, capacitance: ExprId, pool: &ExprPool) -> Component {
242    let port_p = Port::new(&format!("{name}.p"), pool);
243    let port_n = Port::new(&format!("{name}.n"), pool);
244
245    // Voltage across: v = port_p.v - port_n.v
246    let v_name = format!("{name}.vc");
247    let v = pool.symbol(&v_name, Domain::Real);
248    let dv_name = format!("d{v_name}/dt");
249    let dv = pool.symbol(&dv_name, Domain::Real);
250
251    // Constitutive: C*dv/dt - i = 0
252    let i = port_p.flow;
253    let c_dv = pool.mul(vec![capacitance, dv]);
254    let eq = pool.add(vec![c_dv, pool.mul(vec![pool.integer(-1_i32), i])]);
255
256    // Voltage definition: v - (port_p.potential - port_n.potential) = 0
257    let neg_vn = pool.mul(vec![pool.integer(-1_i32), port_n.potential]);
258    let v_def = pool.add(vec![
259        v,
260        pool.mul(vec![
261            pool.integer(-1_i32),
262            pool.add(vec![port_p.potential, neg_vn]),
263        ]),
264    ]);
265
266    Component::new(name)
267        .add_port(port_p)
268        .add_port(port_n)
269        .add_equation(eq)
270        .add_equation(v_def)
271        .add_internal_var(v, dv)
272}
273
274/// Create an ideal voltage source: `port_p.potential - port_n.potential - V = 0`
275pub fn voltage_source(name: &str, voltage: ExprId, pool: &ExprPool) -> Component {
276    let port_p = Port::new(&format!("{name}.p"), pool);
277    let port_n = Port::new(&format!("{name}.n"), pool);
278
279    let neg_v = pool.mul(vec![pool.integer(-1_i32), port_n.potential]);
280    let diff_v = pool.add(vec![port_p.potential, neg_v]);
281    let neg_voltage = pool.mul(vec![pool.integer(-1_i32), voltage]);
282    let eq = pool.add(vec![diff_v, neg_voltage]);
283
284    Component::new(name)
285        .add_port(port_p)
286        .add_port(port_n)
287        .add_equation(eq)
288}
289
290// ---------------------------------------------------------------------------
291// Tests
292// ---------------------------------------------------------------------------
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use crate::kernel::ExprPool;
298
299    fn p() -> ExprPool {
300        ExprPool::new()
301    }
302
303    #[test]
304    fn port_creation() {
305        let pool = p();
306        let port = Port::new("pin1", &pool);
307        assert_eq!(port.name, "pin1");
308        // Variables are distinct
309        assert_ne!(port.potential, port.flow);
310    }
311
312    #[test]
313    fn resistor_has_one_equation() {
314        let pool = p();
315        let r_val = pool.symbol("R", crate::kernel::Domain::Real);
316        let comp = resistor("R1", r_val, &pool);
317        assert_eq!(comp.equations.len(), 1);
318        assert_eq!(comp.ports.len(), 2);
319    }
320
321    #[test]
322    fn capacitor_has_two_equations() {
323        let pool = p();
324        let c_val = pool.symbol("C", crate::kernel::Domain::Real);
325        let comp = capacitor("C1", c_val, &pool);
326        assert_eq!(comp.equations.len(), 2);
327        assert_eq!(comp.internal_vars.len(), 1);
328    }
329
330    #[test]
331    fn system_flatten_rc_circuit() {
332        // Simple RC: source → resistor → capacitor → ground
333        let pool = p();
334        let r_val = pool.symbol("R", crate::kernel::Domain::Real);
335        let c_val = pool.symbol("C", crate::kernel::Domain::Real);
336        let v_src = pool.symbol("Vs", crate::kernel::Domain::Real);
337        let t = pool.symbol("t", crate::kernel::Domain::Real);
338
339        let res = resistor("R1", r_val, &pool);
340        let cap = capacitor("C1", c_val, &pool);
341        let src = voltage_source("V1", v_src, &pool);
342
343        let src_p = src.port("V1.p").unwrap().clone();
344        let src_n = src.port("V1.n").unwrap().clone();
345        let res_p = res.port("R1.p").unwrap().clone();
346        let res_n = res.port("R1.n").unwrap().clone();
347        let cap_p = cap.port("C1.p").unwrap().clone();
348        let cap_n = cap.port("C1.n").unwrap().clone();
349
350        let mut sys = System::new();
351        sys.add_component(src);
352        sys.add_component(res);
353        sys.add_component(cap);
354        sys.connect(&src_p, &res_p);
355        sys.connect(&res_n, &cap_p);
356        sys.connect(&cap_n, &src_n);
357
358        let dae = sys.flatten(t, &pool);
359        // Should have equations from components + 2 per connection
360        assert!(dae.n_equations() >= 3); // at least R + C + source equations
361        assert!(dae.n_variables() >= 4); // at least 4 port variables
362    }
363
364    #[test]
365    fn connect_generates_two_equations() {
366        let pool = p();
367        let t = pool.symbol("t", crate::kernel::Domain::Real);
368        let r_val = pool.symbol("R", crate::kernel::Domain::Real);
369        let comp1 = resistor("R1", r_val, &pool);
370        let comp2 = resistor("R2", r_val, &pool);
371        let port_a = comp1.port("R1.n").unwrap().clone();
372        let port_b = comp2.port("R2.p").unwrap().clone();
373        let mut sys = System::new();
374        sys.add_component(comp1);
375        sys.add_component(comp2);
376        sys.connect(&port_a, &port_b);
377        let dae = sys.flatten(t, &pool);
378        // 2 component equations + 2 connection equations = 4
379        assert_eq!(dae.n_equations(), 4);
380    }
381}