alice-physics 3.0.1

Deterministic physics engine: 128-bit fixed-point rigid bodies with bit-exact, replayable results, typed observation and rollback for search and planning, plus FEM, CFD and 3D-print analysis modules
Documentation
//! Piezoelectric element with bidirectional force ↔ voltage coupling.
//!
//! Complements the mechanical stress modules by exposing a first-order
//! constitutive relation for a compression / tension piezoelectric
//! element (PZT ceramic, quartz, PVDF film):
//!
//! ```text
//! direct effect  : V = (d · σ · thickness) / permittivity
//! converse effect: strain = d · E
//! ```
//!
//! where `d` is the piezoelectric charge coefficient, `σ = F / A` is
//! the applied stress, `E = V / thickness` is the electric field, and
//! `permittivity = ε_r · ε_0`. Both directions are exposed so a
//! caller can drive an actuator with a voltage and read back the
//! displacement, or read a sensor's open-circuit voltage under an
//! external load.

/// Material constants for a piezoelectric element.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PiezoElement {
    /// Piezoelectric charge coefficient `d` (C/N or m/V).
    pub d_coefficient: f32,
    /// Cross-sectional area (m²).
    pub area_m2: f32,
    /// Element thickness along the poling direction (m).
    pub thickness_m: f32,
    /// Relative permittivity `ε_r`.
    pub relative_permittivity: f32,
    /// Young's modulus (Pa) for the mechanical spring model. Used by
    /// `strain_under_stress` to convert stress to strain when the
    /// caller does not supply their own material.
    pub youngs_modulus_pa: f32,
}

/// Vacuum permittivity `ε_0 ≈ 8.8541878e-12 F/m` (truncated to
/// `f32` precision).
const EPSILON_0: f32 = 8.854_188e-12;

impl PiezoElement {
    /// PZT-5A ceramic — standard sensor / actuator material.
    #[must_use]
    pub fn pzt_5a(area_m2: f32, thickness_m: f32) -> Self {
        Self {
            d_coefficient: 3.74e-10,
            area_m2,
            thickness_m,
            relative_permittivity: 1700.0,
            youngs_modulus_pa: 61.0e9,
        }
    }

    /// Quartz — high-Q, temperature-stable oscillator.
    #[must_use]
    pub fn quartz(area_m2: f32, thickness_m: f32) -> Self {
        Self {
            d_coefficient: 2.3e-12,
            area_m2,
            thickness_m,
            relative_permittivity: 4.5,
            youngs_modulus_pa: 78.0e9,
        }
    }

    /// PVDF polymer — flexible, low-density.
    #[must_use]
    pub fn pvdf(area_m2: f32, thickness_m: f32) -> Self {
        Self {
            d_coefficient: 2.1e-11,
            area_m2,
            thickness_m,
            relative_permittivity: 12.0,
            youngs_modulus_pa: 3.0e9,
        }
    }

    /// Absolute permittivity `ε = ε_r · ε_0` (F/m).
    #[must_use]
    pub fn permittivity(&self) -> f32 {
        self.relative_permittivity * EPSILON_0
    }

    /// Open-circuit voltage produced by a compressive force `force_n`
    /// applied along the poling direction.
    ///
    /// `V = d · σ · thickness / permittivity = d · F · thickness / (A · ε)`.
    ///
    /// Rejects `area_m2 <= 0`, `permittivity() <= 0`, and (by symmetry with
    /// [`Self::force_from_voltage`]'s own thickness guard) `thickness_m <=
    /// 0`: a non-positive thickness would otherwise flip the sign of the
    /// result instead of being treated as invalid geometry.
    #[must_use]
    pub fn voltage_from_force(&self, force_n: f32) -> f32 {
        if self.area_m2 <= 0.0 || self.permittivity() <= 0.0 || self.thickness_m <= 0.0 {
            return 0.0;
        }
        let stress = force_n / self.area_m2;
        stress * self.d_coefficient * self.thickness_m / self.permittivity()
    }

    /// Force generated by driving the element with `voltage_v` in the
    /// converse-piezoelectric direction.
    ///
    /// The converse effect gives a free-strain `d · E` which, when
    /// the element is fully constrained, translates to a blocked
    /// force `F = E_field · d · Y · A = V · d · Y · A / thickness`.
    ///
    /// Rejects `thickness_m <= 0` and (by symmetry with
    /// [`Self::voltage_from_force`]'s own area guard) `area_m2 <= 0`: a
    /// non-positive area would otherwise flip the sign of the result
    /// instead of being treated as invalid geometry.
    #[must_use]
    pub fn force_from_voltage(&self, voltage_v: f32) -> f32 {
        if self.thickness_m <= 0.0 || self.area_m2 <= 0.0 {
            return 0.0;
        }
        let e_field = voltage_v / self.thickness_m;
        e_field * self.d_coefficient * self.youngs_modulus_pa * self.area_m2
    }

    /// Strain produced by an applied stress via the mechanical
    /// constitutive law `strain = stress / Y`.
    #[must_use]
    pub fn strain_under_stress(&self, stress_pa: f32) -> f32 {
        if self.youngs_modulus_pa <= 0.0 {
            return 0.0;
        }
        stress_pa / self.youngs_modulus_pa
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn pzt_generates_voltage_under_compression() {
        let e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        let v = e.voltage_from_force(10.0);
        assert!(v > 0.0);
    }

    #[test]
    fn negative_force_yields_negative_voltage() {
        let e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        let v = e.voltage_from_force(-10.0);
        assert!(v < 0.0);
    }

    #[test]
    fn force_scales_linearly_with_voltage() {
        let e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        let f1 = e.force_from_voltage(10.0);
        let f2 = e.force_from_voltage(20.0);
        assert!((f2 - 2.0 * f1).abs() / f1 < 1.0e-3);
    }

    #[test]
    fn pvdf_less_stiff_than_pzt() {
        let pvdf = PiezoElement::pvdf(1.0e-4, 2.0e-3);
        let pzt = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        assert!(pvdf.youngs_modulus_pa < pzt.youngs_modulus_pa);
    }

    #[test]
    fn permittivity_matches_relative_times_epsilon_zero() {
        let e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        let expected = 1700.0 * EPSILON_0;
        assert!((e.permittivity() - expected).abs() < 1.0e-15);
    }

    #[test]
    fn zero_area_yields_zero_voltage() {
        let mut e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        e.area_m2 = 0.0;
        assert_eq!(e.voltage_from_force(10.0), 0.0);
    }

    #[test]
    fn zero_thickness_yields_zero_force() {
        let mut e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        e.thickness_m = 0.0;
        assert_eq!(e.force_from_voltage(10.0), 0.0);
    }

    #[test]
    fn strain_under_stress_matches_hookes_law() {
        let e = PiezoElement::pzt_5a(1.0e-4, 2.0e-3);
        let stress = 10.0e6; // 10 MPa
        let strain = e.strain_under_stress(stress);
        assert!((strain - stress / 61.0e9).abs() < 1.0e-12);
    }
}