libpower/transform/
ipark.rs

1//! # Inverse Park Transformation (dq0 → αβ0)
2//!
3//! This module implements the inverse Park transformation, which converts rotating
4//! reference frame quantities (dq coordinates) back into two-phase orthogonal
5//! stationary reference frame quantities (αβ coordinates). This transformation
6//! is essential for converting control signals from the rotating frame back to
7//! the stationary frame for implementation in three-phase systems.
8//!
9//! ## Theory
10//!
11//! The inverse Park transformation converts rotating dq components back into stationary
12//! αβ components. This is the mathematical inverse of the Park transformation and is
13//! crucial for implementing field-oriented control where control is performed in the
14//! dq frame but actuation must be in the abc frame.
15//!
16//! ### Mathematical Definition
17//!
18//! **Inverse Park Transform (dq0 → αβ0):**
19//! ```text
20//! [α]   [cos(θ)  -sin(θ)  0] [d]
21//! [β] = [sin(θ)   cos(θ)  0] [q]
22//! [0]   [0        0       1] [0]
23//! ```
24//!
25//! ### Expanded Form
26//!
27//! ```text
28//! α = d×cos(θ) - q×sin(θ)     // Alpha component in stationary frame
29//! β = d×sin(θ) + q×cos(θ)     // Beta component in stationary frame  
30//! 0 = 0                       // Zero-sequence unchanged
31//! ```
32//!
33//! ## Physical Interpretation
34//!
35//! - **d component**: DC magnitude aligned with rotating reference
36//! - **q component**: DC magnitude 90° ahead of rotating reference
37//! - **θ (theta)**: Rotation angle of the reference frame
38//! - **α, β outputs**: Sinusoidal quantities in stationary frame
39//!
40//! ## Control Flow in Field-Oriented Control
41//!
42//! 1. **Measurement**: abc → Clarke → αβ → Park → dq (DC quantities)
43//! 2. **Control**: PI controllers operate on DC dq quantities
44//! 3. **Actuation**: dq → Inverse Park → αβ → Inverse Clarke → abc
45//!
46//! ## Applications
47//!
48//! - **Vector control of AC motors**: Converting dq voltage commands to αβ
49//! - **Grid-tied inverters**: Converting dq current references to αβ
50//! - **Active power filters**: Converting rotating frame commands to stationary
51//! - **Wind turbine control**: Grid synchronization and power control
52//! - **Battery energy storage**: Grid interface voltage/current generation
53//! - **Motor drive PWM generation**: Creating three-phase modulation signals
54
55/// Inverse Park transformation for converting dq0 coordinates to αβ0 stationary coordinates.
56///
57/// This structure maintains the state for inverse Park transformation calculations,
58/// storing both input (dq0) and output (αβ0) values along with the rotation angle.
59/// The transformation converts rotating reference frame quantities back to stationary
60/// reference frame quantities, enabling physical implementation of control commands.
61///
62/// # Examples
63///
64/// ## Basic Motor Control Application
65///
66/// ```rust
67/// use libpower::transform::ipark::IPark;
68///
69/// // Create inverse Park transformer
70/// let mut ipark = IPark::new(0.0, 0.0);
71///
72/// // Set dq control outputs from PI controllers
73/// ipark.set_inputs(10.0, 5.0, 0.0);  // id = 10A, iq = 5A (DC values)
74///
75/// // Set rotation angle from rotor position
76/// let theta = std::f32::consts::PI / 6.0;  // 30° electrical
77/// ipark.set_angle(theta.sin(), theta.cos());
78/// ipark.calculate();
79///
80/// // Get αβ voltages for PWM generation
81/// let v_alpha = ipark.get_alpha();  // Stationary frame voltage
82/// let v_beta = ipark.get_beta();    // Stationary frame voltage
83///
84/// println!("v_α: {:.3}, v_β: {:.3}", v_alpha, v_beta);
85/// ```
86///
87/// ## Field-Oriented Control Example
88///
89/// ```rust
90/// use libpower::transform::ipark::IPark;
91///
92/// let mut ipark = IPark::new(0.0, 0.0);
93///
94/// // Motor control scenario: pure torque command (q-axis only)
95/// let id_ref = 0.0;   // No flux current (field weakening off)
96/// let iq_ref = 15.0;  // 15A torque current command
97///
98/// ipark.set_inputs(id_ref, iq_ref, 0.0);
99///
100/// // Rotor position from encoder/estimator
101/// let rotor_angle = 0.0f32;  // Aligned with d-axis
102/// ipark.set_angle(rotor_angle.sin(), rotor_angle.cos());
103/// ipark.calculate();
104///
105/// // Pure q-axis current becomes pure β-axis current when θ = 0°
106/// assert_eq!(ipark.get_alpha(), 0.0);   // No α component
107/// assert_eq!(ipark.get_beta(), 15.0);   // All current in β
108/// ```
109///
110/// ## Sinusoidal Generation
111///
112/// ```rust
113/// use libpower::transform::ipark::IPark;
114///
115/// let mut ipark = IPark::new(0.0, 0.0);
116///
117/// // Generate sinusoidal output by rotating a constant dq vector
118/// let magnitude = 100.0;  // 100V amplitude
119/// let d_component = magnitude;  // Constant d-axis
120/// let q_component = 0.0;        // No q-axis
121///
122/// ipark.set_inputs(d_component, q_component, 0.0);
123///
124/// // Simulate rotation over one electrical cycle
125/// for step in 0..8 {
126///     let angle = step as f32 * std::f32::consts::PI / 4.0;  // 45° steps
127///     ipark.set_angle(angle.sin(), angle.cos());
128///     ipark.calculate();
129///     
130///     println!("θ: {:.0}° → α: {:.1}, β: {:.1}",
131///              angle * 180.0 / std::f32::consts::PI,
132///              ipark.get_alpha(), ipark.get_beta());
133/// }
134/// // This generates: α = magnitude×cos(θ), β = magnitude×sin(θ)
135/// ```
136///
137/// ## Motor Control Voltage Generation
138///
139/// ```rust
140/// use libpower::transform::ipark::IPark;
141///
142/// let mut ipark = IPark::new(0.0, 0.0);
143///
144/// // Set desired d-q voltages (typical FOC)
145/// ipark.set_inputs(10.0, 5.0, 0.0);  // Vd=10V, Vq=5V
146///
147/// let rotor_angle = 0.0f32;  // Aligned with d-axis
148/// ipark.set_angle(rotor_angle.sin(), rotor_angle.cos());
149/// ipark.calculate();
150///
151/// // α-axis should equal d-axis when aligned (θ=0)
152/// assert!((ipark.get_alpha() - 10.0).abs() < 0.01);
153/// // β-axis should equal q-axis when aligned
154/// assert!((ipark.get_beta() - 5.0).abs() < 0.01);
155/// ```
156#[derive(Debug, Clone)]
157pub struct IPark {
158    /// Alpha component output (α) - stationary frame
159    alpha: f32,
160    /// Beta component output (β) - stationary frame
161    beta: f32,
162    /// Zero-sequence component (0) - unchanged
163    zero: f32,
164    /// Sine of rotation angle (sin θ)
165    sin: f32,
166    /// Cosine of rotation angle (cos θ)
167    cos: f32,
168    /// Direct axis component input (d) - rotating frame
169    d: f32,
170    /// Quadrature axis component input (q) - rotating frame
171    q: f32,
172    /// Zero-sequence component input (z) - unchanged
173    z: f32,
174}
175
176impl IPark {
177    /// Creates a new inverse Park transformer with specified αβ initial values.
178    ///
179    /// The transformer is initialized with the given alpha and beta components
180    /// for compatibility with the existing API. The rotation angle (sin/cos) and
181    /// dq inputs must be set before performing calculations.
182    ///
183    /// # Parameters
184    ///
185    /// * `alpha` - Initial alpha component (not used for dq → αβ transformation)
186    /// * `beta` - Initial beta component (not used for dq → αβ transformation)
187    ///
188    /// # Examples
189    ///
190    /// ```rust
191    /// use libpower::transform::ipark::IPark;
192    ///
193    /// // Create transformer (initial αβ values are overwritten by calculation)
194    /// let ipark = IPark::new(0.0, 0.0);
195    /// assert_eq!(ipark.get_d(), 0.0);
196    /// assert_eq!(ipark.get_q(), 0.0);
197    /// ```
198    pub fn new(alpha: f32, beta: f32) -> IPark {
199        IPark {
200            alpha,
201            beta,
202            zero: 0.0,
203            sin: 0.0,
204            cos: 1.0, // Initialize to θ = 0° (cos = 1, sin = 0)
205            d: 0.0,
206            q: 0.0,
207            z: 0.0,
208        }
209    }
210
211    /// Sets the input dq0 components for transformation.
212    ///
213    /// This method sets the rotating frame quantities that will be converted
214    /// back to stationary frame quantities. These are typically the outputs
215    /// from dq-frame controllers.
216    ///
217    /// # Parameters
218    ///
219    /// * `d` - Direct axis component (d) - typically flux-related
220    /// * `q` - Quadrature axis component (q) - typically torque-related
221    /// * `z` - Zero-sequence component (z) - unchanged
222    ///
223    /// # Examples
224    ///
225    /// ```rust
226    /// use libpower::transform::ipark::IPark;
227    ///
228    /// let mut ipark = IPark::new(0.0, 0.0);
229    /// ipark.set_inputs(10.0, 5.0, 0.0);  // Set dq control outputs
230    ///
231    /// assert_eq!(ipark.get_d(), 10.0);
232    /// assert_eq!(ipark.get_q(), 5.0);
233    /// assert_eq!(ipark.get_z(), 0.0);
234    /// ```
235    pub fn set_inputs(&mut self, d: f32, q: f32, z: f32) {
236        self.d = d;
237        self.q = q;
238        self.z = z;
239    }
240
241    /// Sets the rotation angle using sin and cos values.
242    ///
243    /// The inverse Park transformation requires the same rotation angle θ used
244    /// in the forward Park transformation. This method accepts pre-calculated
245    /// sine and cosine values for computational efficiency.
246    ///
247    /// # Parameters
248    ///
249    /// * `sin_theta` - Sine of rotation angle (sin θ)
250    /// * `cos_theta` - Cosine of rotation angle (cos θ)
251    ///
252    /// # Design Notes
253    ///
254    /// - In motor control: θ is typically the electrical rotor angle
255    /// - In grid applications: θ comes from PLL tracking grid phase
256    /// - Must be the same angle used in forward Park transformation
257    ///
258    /// # Examples
259    ///
260    /// ```rust
261    /// use libpower::transform::ipark::IPark;
262    ///
263    /// let mut ipark = IPark::new(0.0, 0.0);
264    ///
265    /// // Set angle for θ = 60° (π/3 radians)
266    /// let theta = std::f32::consts::PI / 3.0;
267    /// ipark.set_angle(theta.sin(), theta.cos());
268    ///
269    /// assert!((ipark.get_sin() - 0.866).abs() < 1e-3);    // sin(60°) ≈ 0.866
270    /// assert!((ipark.get_cos() - 0.5).abs() < 1e-6);      // cos(60°) = 0.5
271    /// ```
272    pub fn set_angle(&mut self, sin_theta: f32, cos_theta: f32) {
273        self.sin = sin_theta;
274        self.cos = cos_theta;
275    }
276
277    /// Gets the alpha component output.
278    ///
279    /// # Returns
280    ///
281    /// The calculated alpha component value in the stationary frame.
282    ///
283    /// # Examples
284    ///
285    /// ```rust
286    /// use libpower::transform::ipark::IPark;
287    ///
288    /// let mut ipark = IPark::new(0.0, 0.0);
289    /// ipark.set_inputs(10.0, 0.0, 0.0);  // Pure d-axis
290    /// ipark.set_angle(0.0, 1.0);         // θ = 0°
291    /// ipark.calculate();
292    /// assert_eq!(ipark.get_alpha(), 10.0);  // d maps to α when θ = 0°
293    /// ```
294    pub fn get_alpha(&self) -> f32 {
295        self.alpha
296    }
297
298    /// Gets the beta component output.
299    ///
300    /// # Returns
301    ///
302    /// The calculated beta component value in the stationary frame.
303    ///
304    /// # Examples
305    ///
306    /// ```rust
307    /// use libpower::transform::ipark::IPark;
308    ///
309    /// let mut ipark = IPark::new(0.0, 0.0);
310    /// ipark.set_inputs(0.0, 10.0, 0.0);  // Pure q-axis
311    /// ipark.set_angle(0.0, 1.0);         // θ = 0°
312    /// ipark.calculate();
313    /// assert_eq!(ipark.get_beta(), 10.0);   // q maps to β when θ = 0°
314    /// ```
315    pub fn get_beta(&self) -> f32 {
316        self.beta
317    }
318
319    /// Gets the zero-sequence component.
320    ///
321    /// # Returns
322    ///
323    /// The zero-sequence component value (currently unused).
324    ///
325    /// # Examples
326    ///
327    /// ```rust
328    /// use libpower::transform::ipark::IPark;
329    ///
330    /// let mut ipark = IPark::new(0.0, 0.0);
331    /// ipark.set_inputs(1.0, 1.0, 0.5);
332    /// assert_eq!(ipark.get_zero(), 0.0);  // Currently not implemented
333    /// ```
334    pub fn get_zero(&self) -> f32 {
335        self.zero
336    }
337
338    /// Gets the sine of the rotation angle.
339    ///
340    /// # Returns
341    ///
342    /// The sine component of the rotation angle (sin θ).
343    ///
344    /// # Examples
345    ///
346    /// ```rust
347    /// use libpower::transform::ipark::IPark;
348    ///
349    /// let mut ipark = IPark::new(0.0, 0.0);
350    /// ipark.set_angle(0.707, 0.707);  // 45° angle
351    /// assert!((ipark.get_sin() - 0.707).abs() < 1e-3);
352    /// ```
353    pub fn get_sin(&self) -> f32 {
354        self.sin
355    }
356
357    /// Gets the cosine of the rotation angle.
358    ///
359    /// # Returns
360    ///
361    /// The cosine component of the rotation angle (cos θ).
362    ///
363    /// # Examples
364    ///
365    /// ```rust
366    /// use libpower::transform::ipark::IPark;
367    ///
368    /// let mut ipark = IPark::new(0.0, 0.0);
369    /// ipark.set_angle(0.707, 0.707);  // 45° angle
370    /// assert!((ipark.get_cos() - 0.707).abs() < 1e-3);
371    /// ```
372    pub fn get_cos(&self) -> f32 {
373        self.cos
374    }
375
376    /// Gets the direct axis (d) component input.
377    ///
378    /// The d-axis component represents the component aligned with the rotating
379    /// reference frame. In motor control, this typically corresponds to the
380    /// flux command from the controller.
381    ///
382    /// # Returns
383    ///
384    /// The current direct axis component input.
385    ///
386    /// # Examples
387    ///
388    /// ```rust
389    /// use libpower::transform::ipark::IPark;
390    ///
391    /// let mut ipark = IPark::new(0.0, 0.0);
392    /// ipark.set_inputs(15.0, 10.0, 0.0);
393    /// assert_eq!(ipark.get_d(), 15.0);
394    /// ```
395    pub fn get_d(&self) -> f32 {
396        self.d
397    }
398
399    /// Gets the quadrature axis (q) component input.
400    ///
401    /// The q-axis component represents the component 90° ahead of the rotating
402    /// reference frame. In motor control, this typically corresponds to the
403    /// torque command from the controller.
404    ///
405    /// # Returns
406    ///
407    /// The current quadrature axis component input.
408    ///
409    /// # Examples
410    ///
411    /// ```rust
412    /// use libpower::transform::ipark::IPark;
413    ///
414    /// let mut ipark = IPark::new(0.0, 0.0);
415    /// ipark.set_inputs(15.0, 10.0, 0.0);
416    /// assert_eq!(ipark.get_q(), 10.0);
417    /// ```
418    pub fn get_q(&self) -> f32 {
419        self.q
420    }
421
422    /// Gets the zero-sequence component input.
423    ///
424    /// # Returns
425    ///
426    /// The zero-sequence component input value.
427    ///
428    /// # Examples
429    ///
430    /// ```rust
431    /// use libpower::transform::ipark::IPark;
432    ///
433    /// let mut ipark = IPark::new(0.0, 0.0);
434    /// ipark.set_inputs(1.0, 1.0, 0.2);
435    /// assert_eq!(ipark.get_z(), 0.2);
436    /// ```
437    pub fn get_z(&self) -> f32 {
438        self.z
439    }
440
441    /// Performs the inverse Park transformation calculation.
442    ///
443    /// This method implements the mathematical inverse Park transformation,
444    /// converting the stored dq0 components into αβ0 stationary reference
445    /// frame quantities using the specified rotation angle.
446    ///
447    /// # Mathematical Implementation
448    ///
449    /// ```text
450    /// α = d×cos(θ) - q×sin(θ)
451    /// β = d×sin(θ) + q×cos(θ)
452    /// 0 = z  (zero-sequence unchanged)
453    /// ```
454    ///
455    /// # Examples
456    ///
457    /// ## Aligned Reference Frame (θ = 0°)
458    ///
459    /// ```rust
460    /// use libpower::transform::ipark::IPark;
461    ///
462    /// let mut ipark = IPark::new(0.0, 0.0);
463    /// ipark.set_inputs(10.0, 5.0, 0.0);  // d = 10, q = 5
464    /// ipark.set_angle(0.0, 1.0);         // θ = 0°: sin = 0, cos = 1
465    /// ipark.calculate();
466    ///
467    /// // When θ = 0°: α = d, β = q
468    /// assert_eq!(ipark.get_alpha(), 10.0);  // d maps to α
469    /// assert_eq!(ipark.get_beta(), 5.0);    // q maps to β
470    /// ```
471    ///
472    /// ## 90° Rotated Reference Frame
473    ///
474    /// ```rust
475    /// use libpower::transform::ipark::IPark;
476    ///
477    /// let mut ipark = IPark::new(0.0, 0.0);
478    /// ipark.set_inputs(10.0, 0.0, 0.0);   // Pure d-axis
479    /// ipark.set_angle(1.0, 0.0);          // θ = 90°: sin = 1, cos = 0
480    /// ipark.calculate();
481    ///
482    /// // When θ = 90°: α = -q, β = d
483    /// assert_eq!(ipark.get_alpha(), 0.0);   // -q = 0
484    /// assert_eq!(ipark.get_beta(), 10.0);   // d = 10
485    /// ```
486    ///
487    /// ## Sinusoidal Generation
488    ///
489    /// ```rust
490    /// use libpower::transform::ipark::IPark;
491    ///
492    /// let mut ipark = IPark::new(0.0, 0.0);
493    ///
494    /// // Generate 100V sine wave by rotating constant d-axis vector
495    /// ipark.set_inputs(100.0, 0.0, 0.0);  // 100V d-axis, 0V q-axis
496    ///
497    /// // θ = 0°: expect α = 100, β = 0
498    /// ipark.set_angle(0.0, 1.0);
499    /// ipark.calculate();
500    /// assert_eq!(ipark.get_alpha(), 100.0);
501    /// assert_eq!(ipark.get_beta(), 0.0);
502    ///
503    /// // θ = 90°: expect α = 0, β = 100  
504    /// ipark.set_angle(1.0, 0.0);
505    /// ipark.calculate();
506    /// assert_eq!(ipark.get_alpha(), 0.0);
507    /// assert_eq!(ipark.get_beta(), 100.0);
508    /// ```
509    ///
510    /// ## Motor Control Voltage Commands
511    ///
512    /// ```rust
513    /// use libpower::transform::ipark::IPark;
514    ///
515    /// let mut ipark = IPark::new(0.0, 0.0);
516    ///
517    /// // PI controller outputs for field-oriented control
518    /// let vd_command = 5.0;   // Flux voltage from Id controller
519    /// let vq_command = 12.0;  // Torque voltage from Iq controller
520    ///
521    /// ipark.set_inputs(vd_command, vq_command, 0.0);
522    ///
523    /// // Rotor angle from position sensor/estimator
524    /// let theta_electrical = std::f32::consts::PI / 3.0;  // 60°
525    /// ipark.set_angle(theta_electrical.sin(), theta_electrical.cos());
526    /// ipark.calculate();
527    ///
528    /// // These αβ voltages feed into space vector PWM
529    /// let v_alpha_ref = ipark.get_alpha();
530    /// let v_beta_ref = ipark.get_beta();
531    ///
532    /// // Verify transformation preserves magnitude
533    /// let dq_magnitude = (vd_command * vd_command + vq_command * vq_command).sqrt();
534    /// let ab_magnitude = (v_alpha_ref * v_alpha_ref + v_beta_ref * v_beta_ref).sqrt();
535    /// assert!((dq_magnitude - ab_magnitude).abs() < 1e-6);
536    /// ```
537    pub fn calculate(&mut self) {
538        // Inverse Park transformation: dq → αβ
539        self.alpha = self.d * self.cos - self.q * self.sin;
540        self.beta = self.q * self.cos + self.d * self.sin;
541        // Note: zero-sequence component could be passed through as:
542        // self.zero = self.z;
543    }
544}
545
546impl Default for IPark {
547    /// Creates an inverse Park transformer with all values set to zero.
548    ///
549    /// The rotation angle is initialized to θ = 0° (cos = 1.0, sin = 0.0).
550    ///
551    /// # Examples
552    ///
553    /// ```rust
554    /// use libpower::transform::ipark::IPark;
555    ///
556    /// let ipark = IPark::default();
557    /// assert_eq!(ipark.get_d(), 0.0);
558    /// assert_eq!(ipark.get_q(), 0.0);
559    /// assert_eq!(ipark.get_cos(), 1.0);   // θ = 0°
560    /// assert_eq!(ipark.get_sin(), 0.0);
561    /// ```
562    fn default() -> Self {
563        Self::new(0.0, 0.0)
564    }
565}