1#[allow(unused_imports)]
6use super::functions::*;
7use std::f64::consts::PI;
8
9#[derive(Debug, Clone, Copy)]
11pub struct MagnetorheologicalFluid {
12 pub base_viscosity: f64,
14 pub tau0_base: f64,
16 pub tau_h_coeff: f64,
18 pub field_exponent: f64,
20 pub volume_fraction: f64,
22}
23impl MagnetorheologicalFluid {
24 pub fn new(
26 base_viscosity: f64,
27 tau0_base: f64,
28 tau_h_coeff: f64,
29 field_exponent: f64,
30 volume_fraction: f64,
31 ) -> Self {
32 Self {
33 base_viscosity,
34 tau0_base,
35 tau_h_coeff,
36 field_exponent,
37 volume_fraction,
38 }
39 }
40 pub fn yield_stress(&self, h_field: f64) -> f64 {
42 self.tau0_base + self.tau_h_coeff * h_field.powf(self.field_exponent)
43 }
44 pub fn shear_stress(&self, shear_rate: f64, h_field: f64) -> f64 {
46 let tau_y = self.yield_stress(h_field);
47 if shear_rate.abs() < 1e-12 {
48 return tau_y;
49 }
50 tau_y + self.base_viscosity * shear_rate
51 }
52 pub fn mason_number(&self, shear_rate: f64, h_field: f64) -> f64 {
55 const MU0: f64 = 4.0 * PI * 1e-7;
56 if h_field < 1e-10 {
57 return f64::INFINITY;
58 }
59 self.base_viscosity * shear_rate / (MU0 * h_field * h_field)
60 }
61 pub fn apparent_viscosity(&self, shear_rate: f64, h_field: f64) -> f64 {
63 if shear_rate.abs() < 1e-12 {
64 return f64::INFINITY;
65 }
66 self.shear_stress(shear_rate, h_field) / shear_rate
67 }
68}
69#[derive(Debug, Clone)]
72pub struct ShapeMemoryAlloy {
73 pub ms: f64,
75 pub mf: f64,
77 pub a_s: f64,
79 pub af: f64,
81 pub xi: f64,
83 pub e_a: f64,
85 pub e_m: f64,
87 pub max_strain: f64,
89 pub cm: f64,
91 pub ca: f64,
93}
94impl ShapeMemoryAlloy {
95 #[allow(clippy::too_many_arguments)]
97 pub fn new(
98 ms: f64,
99 mf: f64,
100 a_s: f64,
101 af: f64,
102 e_a: f64,
103 e_m: f64,
104 max_strain: f64,
105 cm: f64,
106 ca: f64,
107 ) -> Self {
108 Self {
109 ms,
110 mf,
111 a_s,
112 af,
113 xi: 1.0,
114 e_a,
115 e_m,
116 max_strain,
117 cm,
118 ca,
119 }
120 }
121 pub fn nitinol() -> Self {
123 Self::new(291.0, 273.0, 307.0, 325.0, 75e9, 28e9, 0.08, 8e6, 13e6)
124 }
125 pub fn elastic_modulus(&self) -> f64 {
127 self.e_a + self.xi * (self.e_m - self.e_a)
128 }
129 pub fn update_phase(&mut self, temperature: f64, stress: f64) -> f64 {
132 let ms_stress = self.ms + stress / self.cm;
133 let mf_stress = self.mf + stress / self.cm;
134 if temperature <= ms_stress && temperature >= mf_stress {
135 let xi_m =
136 (1.0 + (PI * (temperature - ms_stress) / (mf_stress - ms_stress)).cos()) / 2.0;
137 self.xi = xi_m.clamp(self.xi, 1.0);
138 }
139 let as_stress = self.a_s + stress / self.ca;
140 let af_stress = self.af + stress / self.ca;
141 if temperature >= as_stress && temperature <= af_stress {
142 let xi_a = self.xi
143 * (1.0 + (PI * (temperature - as_stress) / (af_stress - as_stress)).cos())
144 / 2.0;
145 self.xi = xi_a.clamp(0.0, self.xi);
146 }
147 if temperature < mf_stress {
148 self.xi = 1.0;
149 }
150 if temperature > af_stress {
151 self.xi = 0.0;
152 }
153 self.xi
154 }
155 pub fn phase(&self) -> SmaPhase {
157 if self.xi > 0.99 {
158 SmaPhase::Martensite
159 } else if self.xi < 0.01 {
160 SmaPhase::Austenite
161 } else {
162 SmaPhase::Mixed
163 }
164 }
165 pub fn recovery_stress(&self, strain: f64) -> f64 {
167 self.elastic_modulus() * (strain - self.xi * self.max_strain)
168 }
169}
170#[derive(Debug, Clone, Copy)]
172pub struct ElectrorheologicalFluid {
173 pub base_viscosity: f64,
175 pub field_coeff: f64,
177 pub saturation_field: f64,
179 pub epsilon_p: f64,
181 pub epsilon_f: f64,
183}
184impl ElectrorheologicalFluid {
185 pub fn new(
187 base_viscosity: f64,
188 field_coeff: f64,
189 saturation_field: f64,
190 epsilon_p: f64,
191 epsilon_f: f64,
192 ) -> Self {
193 Self {
194 base_viscosity,
195 field_coeff,
196 saturation_field,
197 epsilon_p,
198 epsilon_f,
199 }
200 }
201 pub fn yield_stress(&self, e_field: f64) -> f64 {
203 let e_eff = e_field.min(self.saturation_field);
204 self.field_coeff * e_eff * e_eff
205 }
206 pub fn shear_stress(&self, shear_rate: f64, e_field: f64) -> f64 {
208 let tau_y = self.yield_stress(e_field);
209 if shear_rate.abs() < 1e-12 {
210 return tau_y;
211 }
212 tau_y + self.base_viscosity * shear_rate
213 }
214 pub fn beta_parameter(&self) -> f64 {
216 (self.epsilon_p - self.epsilon_f) / (self.epsilon_p + 2.0 * self.epsilon_f)
217 }
218}
219#[derive(Debug, Clone, Copy)]
221pub struct PolymerActuator {
222 pub curvature_gain: f64,
224 pub time_constant: f64,
226 pub length: f64,
228 pub thickness: f64,
230 pub blocking_force_gain: f64,
232 pub bending_state: f64,
234}
235impl PolymerActuator {
236 pub fn new(
238 curvature_gain: f64,
239 time_constant: f64,
240 length: f64,
241 thickness: f64,
242 blocking_force_gain: f64,
243 ) -> Self {
244 Self {
245 curvature_gain,
246 time_constant,
247 length,
248 thickness,
249 blocking_force_gain,
250 bending_state: 0.0,
251 }
252 }
253 pub fn steady_state_deflection(&self, voltage: f64) -> f64 {
255 let kappa = self.curvature_gain * voltage;
256 kappa * self.length * self.length / 2.0
257 }
258 pub fn transient_deflection(&self, voltage: f64, t: f64) -> f64 {
260 let delta_ss = self.steady_state_deflection(voltage);
261 delta_ss * (1.0 - (-t / self.time_constant).exp())
262 }
263 pub fn blocking_force(&self, voltage: f64) -> f64 {
265 self.blocking_force_gain * voltage
266 }
267 pub fn curvature(&self, voltage: f64) -> f64 {
269 self.curvature_gain * voltage
270 }
271}
272#[derive(Debug, Clone, Copy)]
274pub struct DielectricElastomer {
275 pub epsilon_r: f64,
277 pub thickness: f64,
279 pub initial_area: f64,
281 pub elastic_modulus: f64,
283 pub breakdown_field: f64,
285}
286impl DielectricElastomer {
287 pub fn new(
289 epsilon_r: f64,
290 thickness: f64,
291 initial_area: f64,
292 elastic_modulus: f64,
293 breakdown_field: f64,
294 ) -> Self {
295 Self {
296 epsilon_r,
297 thickness,
298 initial_area,
299 elastic_modulus,
300 breakdown_field,
301 }
302 }
303 pub fn maxwell_stress(&self, e_field: f64) -> f64 {
305 const EPS0: f64 = 8.854e-12;
306 self.epsilon_r * EPS0 * e_field * e_field
307 }
308 pub fn actuation_strain(&self, voltage: f64) -> f64 {
310 let e_field = voltage / self.thickness;
311 let e_eff = e_field.min(self.breakdown_field);
312 let p = self.maxwell_stress(e_eff);
313 (p / self.elastic_modulus).min(0.9)
314 }
315 pub fn area_strain(&self, voltage: f64) -> f64 {
317 2.0 * self.actuation_strain(voltage)
318 }
319 pub fn actuation_pressure(&self, voltage: f64) -> f64 {
321 let e_field = (voltage / self.thickness).min(self.breakdown_field);
322 self.maxwell_stress(e_field)
323 }
324 pub fn electric_field(&self, voltage: f64) -> f64 {
326 voltage / self.thickness
327 }
328}
329#[derive(Debug, Clone)]
331pub struct SmaController {
332 pub sma: ShapeMemoryAlloy,
334 pub target_xi: f64,
336 pub t_hot: f64,
338 pub t_cold: f64,
340 pub kp: f64,
342 pub temperature: f64,
344 pub max_power: f64,
346}
347impl SmaController {
348 pub fn new(sma: ShapeMemoryAlloy, t_hot: f64, t_cold: f64, kp: f64, max_power: f64) -> Self {
350 let temperature = t_cold;
351 Self {
352 sma,
353 target_xi: 0.0,
354 t_hot,
355 t_cold,
356 kp,
357 temperature,
358 max_power,
359 }
360 }
361 pub fn set_target(&mut self, xi_target: f64) {
363 self.target_xi = xi_target.clamp(0.0, 1.0);
364 }
365 pub fn compute_power(&self) -> f64 {
367 let xi_err = self.target_xi - self.sma.xi;
368 if xi_err > 0.1 {
369 0.0
370 } else if xi_err < -0.1 {
371 self.max_power
372 } else {
373 (-self.kp * xi_err * self.max_power).clamp(0.0, self.max_power)
374 }
375 }
376 pub fn step(&mut self, dt: f64, r_thermal: f64, t_ambient: f64) {
379 let power = self.compute_power();
380 let thermal_mass = 1e-4;
381 let q_loss = (self.temperature - t_ambient) / r_thermal;
382 let d_temp = (power - q_loss) / thermal_mass * dt;
383 self.temperature = (self.temperature + d_temp).clamp(self.t_cold, self.t_hot);
384 self.sma.update_phase(self.temperature, 0.0);
385 }
386}
387#[derive(Debug, Clone)]
389pub struct SmartStructure {
390 pub n_modes: usize,
392 pub modal_frequencies: Vec<f64>,
394 pub modal_damping: Vec<f64>,
396 pub actuator_positions: Vec<f64>,
398 pub sensor_positions: Vec<f64>,
400 pub modal_state: Vec<[f64; 2]>,
402 pub control_gains: Vec<f64>,
404}
405impl SmartStructure {
406 pub fn new(
408 n_modes: usize,
409 modal_frequencies: Vec<f64>,
410 modal_damping: Vec<f64>,
411 actuator_positions: Vec<f64>,
412 sensor_positions: Vec<f64>,
413 ) -> Self {
414 let control_gains = vec![1.0; n_modes];
415 let modal_state = vec![[0.0; 2]; n_modes];
416 Self {
417 n_modes,
418 modal_frequencies,
419 modal_damping,
420 actuator_positions,
421 sensor_positions,
422 modal_state,
423 control_gains,
424 }
425 }
426 fn mode_shape(&self, mode: usize, position: f64) -> f64 {
428 ((mode + 1) as f64 * PI * position).sin()
429 }
430 pub fn modal_control(&self, sensor_readings: &[f64]) -> Vec<f64> {
432 let mut u = vec![0.0; self.actuator_positions.len()];
433 for (k, &x_act) in self.actuator_positions.iter().enumerate() {
434 for m in 0..self.n_modes {
435 let obs: f64 = sensor_readings
436 .iter()
437 .zip(self.sensor_positions.iter())
438 .map(|(&y, &x_s)| y * self.mode_shape(m, x_s))
439 .sum();
440 let vel = self.modal_state[m][1];
441 let phi_act = self.mode_shape(m, x_act);
442 u[k] -= self.control_gains[m] * (obs + vel) * phi_act;
443 }
444 }
445 u
446 }
447 pub fn step(&mut self, forcing: &[f64], dt: f64) {
449 for m in 0..self.n_modes {
450 let omega = 2.0 * PI * self.modal_frequencies[m];
451 let zeta = self.modal_damping[m];
452 let q = self.modal_state[m][0];
453 let qdot = self.modal_state[m][1];
454 let f_gen: f64 = forcing
455 .iter()
456 .zip(self.actuator_positions.iter())
457 .map(|(&f, &x)| f * self.mode_shape(m, x))
458 .sum();
459 let qddot = f_gen - 2.0 * zeta * omega * qdot - omega * omega * q;
460 self.modal_state[m][0] = q + qdot * dt;
461 self.modal_state[m][1] = qdot + qddot * dt;
462 }
463 }
464 pub fn displacement_at(&self, x: f64) -> f64 {
466 (0..self.n_modes)
467 .map(|m| self.modal_state[m][0] * self.mode_shape(m, x))
468 .sum()
469 }
470 pub fn set_control_gains(&mut self, gains: Vec<f64>) {
472 let n = gains.len().min(self.n_modes);
473 self.control_gains[..n].copy_from_slice(&gains[..n]);
474 }
475}
476#[derive(Debug, Clone, Copy)]
480pub struct HydrogelSwelling {
481 pub chi: f64,
483 pub phi0: f64,
485 pub n_c: f64,
487 pub v_s: f64,
489 pub temperature: f64,
491}
492impl HydrogelSwelling {
493 pub fn new(chi: f64, phi0: f64, n_c: f64, v_s: f64, temperature: f64) -> Self {
495 Self {
496 chi,
497 phi0,
498 n_c,
499 v_s,
500 temperature,
501 }
502 }
503 pub fn pnipam() -> Self {
505 Self::new(0.45, 0.05, 100.0, 18e-6, 298.0)
506 }
507 pub fn mixing_free_energy(&self, phi: f64) -> f64 {
511 const R: f64 = 8.314;
512 let phi = phi.clamp(1e-6, 1.0 - 1e-6);
513 let psi = 1.0 - phi;
514 (R * self.temperature / self.v_s) * (phi * phi.ln() + psi * psi.ln() + self.chi * phi * psi)
515 }
516 pub fn equilibrium_phi(&self) -> f64 {
520 let phi_guess = self.phi0;
521 let mut phi = phi_guess.clamp(0.01, 0.99);
522 for _ in 0..50 {
523 let psi = 1.0 - phi;
524 let mu_mix = phi.ln() + psi + self.chi * psi * psi;
525 let mu_el = (phi / (2.0 * self.n_c)) * (phi / self.phi0).powf(1.0 / 3.0);
526 let mu = mu_mix + mu_el;
527 let d_mu_mix = 1.0 / phi - 1.0 - 2.0 * self.chi * psi;
528 let d_mu_el = (1.0 / (2.0 * self.n_c))
529 * (phi / self.phi0).powf(1.0 / 3.0)
530 * (1.0 + phi / (3.0 * self.phi0));
531 let d_mu = d_mu_mix + d_mu_el;
532 if d_mu.abs() < 1e-15 {
533 break;
534 }
535 phi -= mu / d_mu;
536 phi = phi.clamp(0.001, 0.999);
537 }
538 phi
539 }
540 pub fn swelling_ratio(&self) -> f64 {
542 1.0 / self.equilibrium_phi().max(1e-6)
543 }
544 pub fn linear_strain(&self) -> f64 {
546 self.swelling_ratio().powf(1.0 / 3.0) - 1.0
547 }
548 pub fn osmotic_pressure(&self, phi: f64) -> f64 {
550 const R: f64 = 8.314;
551 let phi = phi.clamp(1e-6, 1.0 - 1e-6);
552 let psi = 1.0 - phi;
553 -(R * self.temperature / self.v_s) * (psi + phi.ln() + self.chi * phi * phi)
554 }
555}
556#[derive(Debug, Clone, Copy, PartialEq)]
558pub enum EapType {
559 Ionic,
561 Electronic,
563}
564#[derive(Debug, Clone)]
569pub struct FerroelectricHysteresis {
570 pub p_sat: f64,
572 pub e_coercive: f64,
574 pub p_remanent: f64,
576 pub n_bins: usize,
578 pub(super) hysteron_states: Vec<i8>,
580 pub(super) hysteron_fields: Vec<[f64; 2]>,
582 pub polarization: f64,
584}
585impl FerroelectricHysteresis {
586 pub fn new(p_sat: f64, e_coercive: f64, p_remanent: f64, n_bins: usize) -> Self {
588 let mut hysteron_fields = Vec::with_capacity(n_bins);
589 let mut hysteron_states = Vec::with_capacity(n_bins);
590 for i in 0..n_bins {
591 let frac = (i as f64 + 0.5) / n_bins as f64;
592 let e_up = e_coercive * (0.5 + 1.5 * frac);
593 let e_down = -e_up * (p_remanent / p_sat).max(0.1);
594 hysteron_fields.push([e_down, e_up]);
595 hysteron_states.push(-1i8);
596 }
597 Self {
598 p_sat,
599 e_coercive,
600 p_remanent,
601 n_bins,
602 hysteron_states,
603 hysteron_fields,
604 polarization: -p_remanent,
605 }
606 }
607 pub fn barium_titanate() -> Self {
609 Self::new(0.26, 0.4e6, 0.18, 32)
610 }
611 pub fn update(&mut self, e_field: f64) {
613 for (i, &[e_down, e_up]) in self.hysteron_fields.iter().enumerate() {
614 if e_field > e_up {
615 self.hysteron_states[i] = 1;
616 } else if e_field < e_down {
617 self.hysteron_states[i] = -1;
618 }
619 }
620 let sum: i32 = self.hysteron_states.iter().map(|&s| s as i32).sum();
621 self.polarization = self.p_sat * sum as f64 / self.n_bins as f64;
622 }
623 pub fn susceptibility(&self, e_field: f64, delta_e: f64) -> f64 {
625 let mut twin = self.clone();
626 twin.update(e_field + delta_e);
627 let p1 = twin.polarization;
628 let mut twin2 = self.clone();
629 twin2.update(e_field - delta_e);
630 let p2 = twin2.polarization;
631 (p1 - p2) / (2.0 * delta_e)
632 }
633 pub fn stored_energy(&self, e_field: f64) -> f64 {
635 const EPS0: f64 = 8.854e-12;
636 EPS0 * e_field * e_field / 2.0 + self.polarization * e_field
637 }
638}
639#[derive(Debug, Clone, Copy)]
641pub struct BimorphActuator {
642 pub length: f64,
644 pub t1: f64,
646 pub t2: f64,
648 pub e1: f64,
650 pub e2: f64,
652 pub alpha1: f64,
654 pub alpha2: f64,
656}
657impl BimorphActuator {
658 pub fn new(length: f64, t1: f64, t2: f64, e1: f64, e2: f64, alpha1: f64, alpha2: f64) -> Self {
660 Self {
661 length,
662 t1,
663 t2,
664 e1,
665 e2,
666 alpha1,
667 alpha2,
668 }
669 }
670 pub fn tip_deflection(&self, delta_t: f64) -> f64 {
673 let m = self.e1 * self.t1 / (self.e2 * self.t2);
674 let n = self.t1 / self.t2;
675 let t_total = self.t1 + self.t2;
676 let denom = 3.0 * (1.0 + m) * (1.0 + m) + (1.0 + m * n) * (m * m + 1.0 / (m * n));
677 if denom.abs() < 1e-20 {
678 return 0.0;
679 }
680 let curvature = 6.0 * (self.alpha1 - self.alpha2) * delta_t * (1.0 + m) / (t_total * denom);
681 curvature * self.length * self.length / 2.0
682 }
683 pub fn neutral_axis(&self) -> f64 {
685 let n1 = self.e1 * self.t1;
686 let n2 = self.e2 * self.t2;
687 (n1 * self.t1 / 2.0 + n2 * (self.t1 + self.t2 / 2.0)) / (n1 + n2)
688 }
689 pub fn curvature_radius(&self, delta_t: f64) -> f64 {
691 let deflection = self.tip_deflection(delta_t);
692 if deflection.abs() < 1e-20 {
693 return f64::INFINITY;
694 }
695 self.length * self.length / (2.0 * deflection)
696 }
697}
698#[derive(Debug, Clone)]
703pub struct SmartComposite {
704 pub fiber_volume_fraction: f64,
706 pub sma: ShapeMemoryAlloy,
708 pub matrix_modulus: f64,
710 pub matrix_density: f64,
712 pub sma_density: f64,
714 pub thickness: f64,
716}
717impl SmartComposite {
718 pub fn new(
720 fiber_volume_fraction: f64,
721 sma: ShapeMemoryAlloy,
722 matrix_modulus: f64,
723 matrix_density: f64,
724 sma_density: f64,
725 thickness: f64,
726 ) -> Self {
727 Self {
728 fiber_volume_fraction,
729 sma,
730 matrix_modulus,
731 matrix_density,
732 sma_density,
733 thickness,
734 }
735 }
736 pub fn longitudinal_modulus(&self) -> f64 {
738 let v_f = self.fiber_volume_fraction;
739 let v_m = 1.0 - v_f;
740 v_f * self.sma.elastic_modulus() + v_m * self.matrix_modulus
741 }
742 pub fn transverse_modulus(&self) -> f64 {
744 let v_f = self.fiber_volume_fraction;
745 let v_m = 1.0 - v_f;
746 1.0 / (v_f / self.sma.elastic_modulus() + v_m / self.matrix_modulus)
747 }
748 pub fn density(&self) -> f64 {
750 let v_f = self.fiber_volume_fraction;
751 let v_m = 1.0 - v_f;
752 v_f * self.sma_density + v_m * self.matrix_density
753 }
754 pub fn composite_recovery_stress(&self, strain: f64) -> f64 {
756 self.fiber_volume_fraction * self.sma.recovery_stress(strain)
757 }
758 pub fn actuation_curvature(&self, temperature: f64) -> f64 {
762 let e_sma = self.sma.elastic_modulus();
763 let e_mat = self.matrix_modulus;
764 let t_sma = self.thickness * self.fiber_volume_fraction;
765 let t_mat = self.thickness * (1.0 - self.fiber_volume_fraction);
766 let eps_sma = self.sma.max_strain * (1.0 - self.sma.xi);
767 let _ = temperature;
768
769 6.0 * e_sma * e_mat * t_sma * t_mat * (t_sma + t_mat) * eps_sma
770 / (e_sma * t_sma.powi(2) * (4.0 * t_sma + 3.0 * t_mat)
771 + e_mat * t_mat.powi(2) * (4.0 * t_mat + 3.0 * t_sma)
772 + 6.0 * e_sma * e_mat * t_sma * t_mat * (t_sma + t_mat))
773 .max(1e-30)
774 }
775}
776#[derive(Debug, Clone)]
784#[allow(dead_code)]
785pub struct BrinsonModel {
786 pub ms: f64,
788 pub mf: f64,
790 pub a_s: f64,
792 pub af: f64,
794 pub xi_s: f64,
796 pub xi_t: f64,
798 pub e_a: f64,
800 pub e_m: f64,
802 pub eps_l: f64,
804 pub cm: f64,
806 pub ca: f64,
808 pub theta: f64,
810 pub stress: f64,
812 pub strain: f64,
814 pub temperature: f64,
816}
817impl BrinsonModel {
818 #[allow(clippy::too_many_arguments)]
820 pub fn new(
821 ms: f64,
822 mf: f64,
823 a_s: f64,
824 af: f64,
825 e_a: f64,
826 e_m: f64,
827 eps_l: f64,
828 cm: f64,
829 ca: f64,
830 theta: f64,
831 ) -> Self {
832 Self {
833 ms,
834 mf,
835 a_s,
836 af,
837 xi_s: 0.0,
838 xi_t: 1.0,
839 e_a,
840 e_m,
841 eps_l,
842 cm,
843 ca,
844 theta,
845 stress: 0.0,
846 strain: 0.0,
847 temperature: mf,
848 }
849 }
850 pub fn nitinol() -> Self {
852 Self::new(
853 291.0, 273.0, 307.0, 325.0, 75e9, 28e9, 0.08, 8e6, 13e6, 0.55e6,
854 )
855 }
856 pub fn total_xi(&self) -> f64 {
858 (self.xi_s + self.xi_t).clamp(0.0, 1.0)
859 }
860 pub fn elastic_modulus(&self) -> f64 {
862 self.e_a + self.total_xi() * (self.e_m - self.e_a)
863 }
864 pub fn update(&mut self, temperature: f64, stress: f64) -> (f64, f64, f64) {
868 self.temperature = temperature;
869 self.stress = stress;
870 let ms_s = self.ms + stress / self.cm;
871 let mf_s = self.mf + stress / self.cm;
872 if temperature <= ms_s && temperature >= mf_s {
873 let cos_arg = PI * (temperature - ms_s) / (mf_s - ms_s);
874 let xi_new = (1.0 + cos_arg.cos()) / 2.0;
875 if xi_new > self.xi_t {
876 self.xi_t = xi_new.min(1.0 - self.xi_s);
877 }
878 } else if temperature < mf_s {
879 self.xi_t = 1.0 - self.xi_s;
880 }
881 let sigma_ms = self.cm * (temperature - self.ms).max(0.0);
882 let sigma_mf = self.cm * (temperature - self.mf).max(0.0);
883 if stress >= sigma_ms && stress <= sigma_mf + 1e-3 && temperature > self.mf {
884 let cos_arg = PI * (stress - sigma_ms) / (sigma_mf - sigma_ms + 1e-6);
885 let xi_s_new = (1.0 - self.xi_s) / 2.0 * (1.0 - cos_arg.cos());
886 self.xi_s = (self.xi_s + xi_s_new).clamp(0.0, 1.0 - self.xi_t);
887 }
888 let as_s = self.a_s + stress / self.ca;
889 let af_s = self.af + stress / self.ca;
890 if temperature >= as_s && temperature <= af_s {
891 let cos_arg = PI * (temperature - as_s) / (af_s - as_s);
892 let xi_new = self.total_xi() / 2.0 * (1.0 + cos_arg.cos());
893 let xi_total = xi_new.clamp(0.0, self.total_xi());
894 let frac = if self.total_xi() > 1e-10 {
895 xi_total / self.total_xi()
896 } else {
897 0.0
898 };
899 self.xi_s = (self.xi_s * frac).clamp(0.0, 1.0);
900 self.xi_t = (self.xi_t * frac).clamp(0.0, 1.0);
901 } else if temperature > af_s {
902 self.xi_s = 0.0;
903 self.xi_t = 0.0;
904 }
905 (self.xi_s, self.xi_t, self.total_xi())
906 }
907 pub fn stress_from_strain(&self, strain: f64, t_ref: f64) -> f64 {
911 let e = self.elastic_modulus();
912 e * (strain - self.eps_l * self.xi_s) + self.theta * (self.temperature - t_ref)
913 }
914 pub fn recovery_stress(&self, strain: f64) -> f64 {
916 self.elastic_modulus() * (strain - self.eps_l * self.xi_s)
917 }
918 pub fn sme_stroke(&self, xi_initial: f64, xi_final: f64, wire_length: f64) -> f64 {
920 let delta_xi_s = xi_initial - xi_final;
921 delta_xi_s * self.eps_l * wire_length
922 }
923}
924#[derive(Debug, Clone, Copy)]
931#[allow(dead_code)]
932pub struct Magnetorheological {
933 pub eta0: f64,
935 pub mu_c: f64,
937 pub m_sat: f64,
939 pub phi: f64,
941 pub n_exp: f64,
943 pub c_yield: f64,
945 pub phi_max: f64,
947 pub intrinsic_viscosity: f64,
949}
950impl Magnetorheological {
951 #[allow(clippy::too_many_arguments)]
953 pub fn new(
954 eta0: f64,
955 mu_c: f64,
956 m_sat: f64,
957 phi: f64,
958 n_exp: f64,
959 c_yield: f64,
960 phi_max: f64,
961 intrinsic_viscosity: f64,
962 ) -> Self {
963 Self {
964 eta0,
965 mu_c,
966 m_sat,
967 phi,
968 n_exp,
969 c_yield,
970 phi_max,
971 intrinsic_viscosity,
972 }
973 }
974 pub fn carbonyl_iron() -> Self {
976 Self::new(0.1, 4.0 * PI * 1e-7, 1.36e6, 0.30, 1.5, 0.3e3, 0.74, 2.5)
977 }
978 pub fn effective_viscosity_off(&self) -> f64 {
980 let ratio = self.phi / self.phi_max;
981 let kd = (1.0 - ratio).powf(-self.intrinsic_viscosity * self.phi_max);
982 self.eta0 * kd.max(1.0)
983 }
984 pub fn yield_stress(&self, h_field: f64) -> f64 {
986 self.c_yield * h_field.powf(self.n_exp)
987 }
988 pub fn mason_number(&self, shear_rate: f64, h_field: f64) -> f64 {
990 const MU0: f64 = 1.2566370614e-6;
991 if h_field < 1e-10 {
992 return f64::INFINITY;
993 }
994 self.eta0 * shear_rate / (MU0 * h_field * h_field)
995 }
996 pub fn shear_stress(&self, shear_rate: f64, h_field: f64) -> f64 {
998 let tau_y = self.yield_stress(h_field);
999 let eta_eff = self.effective_viscosity_off();
1000 if shear_rate.abs() < 1e-12 {
1001 return tau_y;
1002 }
1003 tau_y + eta_eff * shear_rate
1004 }
1005 pub fn apparent_viscosity(&self, shear_rate: f64, h_field: f64) -> f64 {
1007 if shear_rate.abs() < 1e-12 {
1008 return f64::INFINITY;
1009 }
1010 self.shear_stress(shear_rate, h_field) / shear_rate
1011 }
1012 pub fn chain_fraction(&self, h_field: f64) -> f64 {
1017 const MU0: f64 = 1.2566370614e-6;
1018 let m = MU0 * self.m_sat * h_field;
1019 let lambda = m / (self.eta0.max(1e-10) + 1.0);
1020 (lambda * self.phi).tanh().clamp(0.0, 1.0)
1021 }
1022 pub fn viscosity_ratio(&self, h_field: f64) -> f64 {
1026 let cf = self.chain_fraction(h_field);
1027 1.0 + 100.0 * cf * self.phi
1028 }
1029}
1030#[derive(Debug, Clone, Copy, PartialEq)]
1032pub enum SmaPhase {
1033 Martensite,
1035 Austenite,
1037 Mixed,
1039}
1040#[derive(Debug, Clone)]
1045pub struct ElectroactivePoly {
1046 pub eap_type: EapType,
1048 pub max_strain: f64,
1050 pub v_half: f64,
1052 pub elastic_modulus: f64,
1054 pub time_constant: f64,
1056 pub state: f64,
1058}
1059impl ElectroactivePoly {
1060 pub fn new(
1062 eap_type: EapType,
1063 max_strain: f64,
1064 v_half: f64,
1065 elastic_modulus: f64,
1066 time_constant: f64,
1067 ) -> Self {
1068 Self {
1069 eap_type,
1070 max_strain,
1071 v_half,
1072 elastic_modulus,
1073 time_constant,
1074 state: 0.0,
1075 }
1076 }
1077 pub fn ipmc() -> Self {
1079 Self::new(EapType::Ionic, 0.03, 1.5, 1e6, 0.1)
1080 }
1081 pub fn dielectric_elastomer() -> Self {
1083 Self::new(EapType::Electronic, 0.45, 1000.0, 1e5, 0.001)
1084 }
1085 pub fn steady_state_strain(&self, voltage: f64) -> f64 {
1087 let k = 4.0 / self.v_half;
1088 let s = 1.0 / (1.0 + (-k * (voltage - self.v_half)).exp());
1089 self.max_strain * s
1090 }
1091 pub fn transient_strain(&self, voltage: f64, t: f64) -> f64 {
1093 let ss = self.steady_state_strain(voltage);
1094 ss * (1.0 - (-t / self.time_constant).exp())
1095 }
1096 pub fn blocking_stress(&self, voltage: f64) -> f64 {
1098 self.elastic_modulus * self.steady_state_strain(voltage)
1099 }
1100 pub fn update(&mut self, voltage: f64, dt: f64) {
1102 let target = self.steady_state_strain(voltage) / self.max_strain.max(1e-12);
1103 let tau = self.time_constant;
1104 self.state += (target - self.state) * (1.0 - (-dt / tau).exp());
1105 self.state = self.state.clamp(0.0, 1.0);
1106 }
1107 pub fn current_strain(&self) -> f64 {
1109 self.state * self.max_strain
1110 }
1111}
1112#[derive(Debug, Clone, Copy)]
1120#[allow(dead_code)]
1121pub struct HydrogelFloryRehner {
1122 pub chi: f64,
1124 pub crosslink_density: f64,
1126 pub phi0: f64,
1128 pub v_s: f64,
1130 pub temperature: f64,
1132 pub dry_modulus: f64,
1134}
1135impl HydrogelFloryRehner {
1136 pub fn new(
1138 chi: f64,
1139 crosslink_density: f64,
1140 phi0: f64,
1141 v_s: f64,
1142 temperature: f64,
1143 dry_modulus: f64,
1144 ) -> Self {
1145 Self {
1146 chi,
1147 crosslink_density,
1148 phi0,
1149 v_s,
1150 temperature,
1151 dry_modulus,
1152 }
1153 }
1154 pub fn polyacrylamide() -> Self {
1156 Self::new(0.45, 10.0, 0.05, 18e-6, 298.0, 1e4)
1157 }
1158 pub fn mixing_free_energy(&self, phi: f64) -> f64 {
1160 const R: f64 = 8.314;
1161 let phi = phi.clamp(1e-6, 1.0 - 1e-6);
1162 let psi = 1.0 - phi;
1163 R * self.temperature / self.v_s * (phi * phi.ln() + psi * psi.ln() + self.chi * phi * psi)
1164 }
1165 pub fn elastic_free_energy(&self, phi: f64) -> f64 {
1169 const R: f64 = 8.314;
1170 let phi = phi.clamp(1e-6, 1.0);
1171 let lambda = (self.phi0 / phi).powf(1.0 / 3.0);
1172 let lambda2 = lambda * lambda;
1173 let ln_lambda3 = 3.0 * lambda.ln();
1174 self.crosslink_density * R * self.temperature / 2.0
1175 * (3.0 * lambda2 - 3.0 - 2.0 * ln_lambda3)
1176 }
1177 pub fn total_free_energy(&self, phi: f64) -> f64 {
1179 self.mixing_free_energy(phi) + self.elastic_free_energy(phi)
1180 }
1181 pub fn chemical_potential(&self, phi: f64) -> f64 {
1185 let phi = phi.clamp(1e-6, 1.0 - 1e-6);
1186 let psi = 1.0 - phi;
1187 psi.ln() + phi + self.chi * phi * phi
1188 }
1189 pub fn elastic_chemical_potential(&self, phi: f64) -> f64 {
1191 let phi = phi.clamp(1e-6, 1.0);
1192 let n = (self.crosslink_density * self.v_s).max(1e-10);
1193 (phi / (2.0 * n)) * (phi / self.phi0).powf(1.0 / 3.0)
1194 }
1195 pub fn equilibrium_swelling(&self) -> f64 {
1197 let mut phi = self.phi0.clamp(0.01, 0.99);
1198 for _ in 0..100 {
1199 let mu_mix = self.chemical_potential(phi);
1200 let mu_el = self.elastic_chemical_potential(phi);
1201 let mu = mu_mix + mu_el;
1202 let dphi = 1e-6;
1203 let d_mu = (self.chemical_potential(phi + dphi)
1204 + self.elastic_chemical_potential(phi + dphi)
1205 - self.chemical_potential(phi - dphi)
1206 - self.elastic_chemical_potential(phi - dphi))
1207 / (2.0 * dphi);
1208 if d_mu.abs() < 1e-15 {
1209 break;
1210 }
1211 phi -= mu / d_mu;
1212 phi = phi.clamp(0.001, 0.999);
1213 }
1214 1.0 / phi.max(1e-6)
1215 }
1216 pub fn linear_strain(&self) -> f64 {
1218 self.equilibrium_swelling().powf(1.0 / 3.0) - 1.0
1219 }
1220 pub fn osmotic_pressure(&self, phi: f64) -> f64 {
1222 const R: f64 = 8.314;
1223 let phi = phi.clamp(1e-6, 1.0 - 1e-6);
1224 let psi = 1.0 - phi;
1225 let mu1 = psi.ln() + phi + self.chi * phi * phi;
1226 -(R * self.temperature / self.v_s) * mu1
1227 }
1228}
1229#[derive(Debug, Clone)]
1237#[allow(dead_code)]
1238pub struct SelfHealingMaterial {
1239 pub virgin_strength: f64,
1241 pub damage: f64,
1243 pub healing_efficiency: f64,
1245 pub k_healing: f64,
1247 pub activation_energy: f64,
1249 pub t_ref: f64,
1251 pub agent_fraction: f64,
1253 pub damage_threshold: f64,
1255 pub time_since_damage: f64,
1257 pub max_efficiency: f64,
1259}
1260impl SelfHealingMaterial {
1261 #[allow(clippy::too_many_arguments)]
1263 pub fn new(
1264 virgin_strength: f64,
1265 k_healing: f64,
1266 activation_energy: f64,
1267 t_ref: f64,
1268 agent_fraction: f64,
1269 damage_threshold: f64,
1270 max_efficiency: f64,
1271 ) -> Self {
1272 Self {
1273 virgin_strength,
1274 damage: 0.0,
1275 healing_efficiency: 0.0,
1276 k_healing,
1277 activation_energy,
1278 t_ref,
1279 agent_fraction,
1280 damage_threshold,
1281 time_since_damage: 0.0,
1282 max_efficiency,
1283 }
1284 }
1285 pub fn microencapsulated_epoxy() -> Self {
1287 Self::new(60e6, 1e-4, 50e3, 298.0, 0.10, 0.05, 0.90)
1288 }
1289 pub fn healing_rate(&self, temperature: f64) -> f64 {
1291 const R: f64 = 8.314;
1292 self.k_healing
1293 * (-self.activation_energy / (R * temperature)).exp()
1294 * (-self.activation_energy / (R * self.t_ref)).exp().recip()
1295 }
1296 pub fn apply_damage(&mut self, damage: f64) {
1298 self.damage = (self.damage + damage).clamp(0.0, 1.0);
1299 if self.damage > self.damage_threshold {
1300 self.time_since_damage = 0.0;
1301 self.healing_efficiency = 0.0;
1302 }
1303 }
1304 pub fn heal(&mut self, dt: f64, temperature: f64) {
1309 if self.damage < self.damage_threshold {
1310 return;
1311 }
1312 let k = self.healing_rate(temperature);
1313 let rate = k * (self.max_efficiency - self.healing_efficiency) * self.agent_fraction;
1314 self.healing_efficiency =
1315 (self.healing_efficiency + rate * dt).clamp(0.0, self.max_efficiency);
1316 self.time_since_damage += dt;
1317 }
1318 pub fn current_strength(&self) -> f64 {
1322 let intact_fraction = 1.0 - self.damage;
1323 let healed_contribution = self.healing_efficiency * self.damage;
1324 self.virgin_strength * (intact_fraction + healed_contribution).clamp(0.0, 1.0)
1325 }
1326 pub fn stiffness_reduction(&self) -> f64 {
1330 (1.0 - self.damage * (1.0 - self.healing_efficiency)).clamp(0.0, 1.0)
1331 }
1332 pub fn toughness_recovery(&self) -> f64 {
1336 self.stiffness_reduction().sqrt()
1337 }
1338 pub fn healing_progress(&self) -> f64 {
1340 if self.max_efficiency < 1e-10 {
1341 0.0
1342 } else {
1343 self.healing_efficiency / self.max_efficiency
1344 }
1345 }
1346}
1347#[derive(Debug, Clone, Copy)]
1349pub struct HydrogelActuator {
1350 pub chi: f64,
1352 pub crosslink_density: f64,
1354 pub q_ref: f64,
1356 pub chi_temp_coeff: f64,
1358 pub ph_ref: f64,
1360 pub chi_ph_coeff: f64,
1362 pub elastic_modulus: f64,
1364}
1365impl HydrogelActuator {
1366 pub fn new(
1368 chi: f64,
1369 crosslink_density: f64,
1370 q_ref: f64,
1371 chi_temp_coeff: f64,
1372 ph_ref: f64,
1373 chi_ph_coeff: f64,
1374 elastic_modulus: f64,
1375 ) -> Self {
1376 Self {
1377 chi,
1378 crosslink_density,
1379 q_ref,
1380 chi_temp_coeff,
1381 ph_ref,
1382 chi_ph_coeff,
1383 elastic_modulus,
1384 }
1385 }
1386 pub fn effective_chi(&self, temperature: f64, ph: f64) -> f64 {
1388 self.chi
1389 + self.chi_temp_coeff * (temperature - 298.0)
1390 + self.chi_ph_coeff * (ph - self.ph_ref)
1391 }
1392 pub fn swelling_ratio(&self, temperature: f64, ph: f64) -> f64 {
1395 let chi_eff = self.effective_chi(temperature, ph);
1396 let q = self.q_ref * (-chi_eff * 0.5).exp();
1397 q.max(1.0)
1398 }
1399 pub fn linear_strain(&self, temperature: f64, ph: f64) -> f64 {
1401 let q = self.swelling_ratio(temperature, ph);
1402 q.powf(1.0 / 3.0) - 1.0
1403 }
1404 pub fn swelling_pressure(&self, temperature: f64, ph: f64) -> f64 {
1406 let strain = self.linear_strain(temperature, ph);
1407 self.elastic_modulus * strain
1408 }
1409 pub fn force(&self, temperature: f64, ph: f64, area: f64) -> f64 {
1411 self.swelling_pressure(temperature, ph) * area
1412 }
1413}
1414#[derive(Debug, Clone, Copy)]
1419pub struct MagnetostrictiveMaterial {
1420 pub lambda_sat: f64,
1422 pub h_sat: f64,
1424 pub elastic_modulus: f64,
1426 pub d33: f64,
1428 pub mu_r: f64,
1430}
1431impl MagnetostrictiveMaterial {
1432 pub fn new(lambda_sat: f64, h_sat: f64, elastic_modulus: f64, d33: f64, mu_r: f64) -> Self {
1434 Self {
1435 lambda_sat,
1436 h_sat,
1437 elastic_modulus,
1438 d33,
1439 mu_r,
1440 }
1441 }
1442 pub fn terfenol_d() -> Self {
1444 Self::new(1600e-6, 60e3, 50e9, 20e-9, 3.0)
1445 }
1446 pub fn strain(&self, h_field: f64) -> f64 {
1448 if h_field.abs() < 1e-10 {
1449 return 0.0;
1450 }
1451 let x = h_field / self.h_sat;
1452 let l = if x.abs() > 30.0 {
1453 x.signum() * (1.0 - 1.0 / x.abs())
1454 } else {
1455 1.0 / x.tanh() - 1.0 / x
1456 };
1457 self.lambda_sat * l * l * h_field.signum().max(0.0)
1458 + self.lambda_sat * l * l * (-h_field.signum()).max(0.0)
1459 }
1460 pub fn strain_magnitude(&self, h_field: f64) -> f64 {
1462 if h_field.abs() < 1e-10 {
1463 return 0.0;
1464 }
1465 let x = h_field.abs() / self.h_sat;
1466 let l = if x > 30.0 {
1467 1.0 - 1.0 / x
1468 } else {
1469 1.0 / x.tanh() - 1.0 / x
1470 };
1471 self.lambda_sat * l * l
1472 }
1473 pub fn blocked_stress(&self, h_field: f64) -> f64 {
1475 self.elastic_modulus * self.strain_magnitude(h_field)
1476 }
1477 pub fn piezomagnetic_coeff(&self, h_field: f64) -> f64 {
1479 let dh = h_field * 1e-4 + 1.0;
1480 (self.strain_magnitude(h_field + dh) - self.strain_magnitude(h_field)) / dh
1481 }
1482}
1483#[derive(Debug, Clone, Copy)]
1488pub struct MagneticShape {
1489 pub max_strain: f64,
1491 pub k_u: f64,
1493 pub m_sat: f64,
1495 pub h_critical: f64,
1497 pub eta: f64,
1499 pub elastic_modulus: f64,
1501}
1502impl MagneticShape {
1503 pub fn new(
1505 max_strain: f64,
1506 k_u: f64,
1507 m_sat: f64,
1508 h_critical: f64,
1509 elastic_modulus: f64,
1510 ) -> Self {
1511 Self {
1512 max_strain,
1513 k_u,
1514 m_sat,
1515 h_critical,
1516 eta: 0.0,
1517 elastic_modulus,
1518 }
1519 }
1520 pub fn ni2mnga() -> Self {
1522 Self::new(0.06, 1.65e5, 600e3, 300e3, 2.0e9)
1523 }
1524 pub fn zeeman_energy(&self, h_field: f64, eta: f64) -> f64 {
1526 const MU0: f64 = 4.0 * PI * 1e-7;
1527 -MU0 * self.m_sat * h_field * eta
1528 }
1529 pub fn anisotropy_energy(&self, eta: f64) -> f64 {
1531 self.k_u * eta * (1.0 - eta)
1532 }
1533 pub fn update(&mut self, h_field: f64) {
1535 if h_field.abs() > self.h_critical {
1536 self.eta = if h_field > 0.0 { 1.0 } else { 0.0 };
1537 } else {
1538 let fraction = (h_field / self.h_critical).clamp(-1.0, 1.0);
1539 self.eta = (0.5 + 0.5 * fraction).clamp(0.0, 1.0);
1540 }
1541 }
1542 pub fn strain(&self) -> f64 {
1544 self.eta * self.max_strain
1545 }
1546 pub fn blocking_stress(&self) -> f64 {
1548 self.elastic_modulus * self.strain()
1549 }
1550}
1551#[derive(Debug, Clone, Copy)]
1564#[allow(dead_code)]
1565pub struct Electrostrictive {
1566 pub epsilon_r: f64,
1568 pub q33: f64,
1570 pub q31: f64,
1572 pub sat_field: f64,
1574 pub elastic_modulus: f64,
1576 pub thickness: f64,
1578}
1579impl Electrostrictive {
1580 pub fn new(
1582 epsilon_r: f64,
1583 q33: f64,
1584 q31: f64,
1585 sat_field: f64,
1586 elastic_modulus: f64,
1587 thickness: f64,
1588 ) -> Self {
1589 Self {
1590 epsilon_r,
1591 q33,
1592 q31,
1593 sat_field,
1594 elastic_modulus,
1595 thickness,
1596 }
1597 }
1598 pub fn pmn_pt() -> Self {
1600 Self::new(5000.0, 0.026, -0.010, 2e7, 6e9, 1e-3)
1601 }
1602 pub fn polarization(&self, e_field: f64) -> f64 {
1606 const EPS0: f64 = 8.854e-12;
1607 let chi = self.epsilon_r - 1.0;
1608 let denom = 1.0 + e_field.abs() / self.sat_field;
1609 EPS0 * chi * e_field / denom
1610 }
1611 pub fn maxwell_stress(&self, e_field: f64) -> f64 {
1615 const EPS0: f64 = 8.854e-12;
1616 0.5 * EPS0 * self.epsilon_r * e_field * e_field
1617 }
1618 pub fn strain_33(&self, e_field: f64) -> f64 {
1622 let p = self.polarization(e_field);
1623 self.q33 * p * p
1624 }
1625 pub fn strain_31(&self, e_field: f64) -> f64 {
1627 let p = self.polarization(e_field);
1628 self.q31 * p * p
1629 }
1630 pub fn thickness_change(&self, voltage: f64) -> f64 {
1632 let e_field = voltage / self.thickness;
1633 self.strain_33(e_field) * self.thickness
1634 }
1635 pub fn blocking_stress(&self, voltage: f64) -> f64 {
1637 let e_field = voltage / self.thickness;
1638 self.elastic_modulus * self.strain_33(e_field)
1639 }
1640 pub fn coupling_coefficient(&self, e_field: f64) -> f64 {
1644 const EPS0: f64 = 8.854e-12;
1645 let p = self.polarization(e_field);
1646 let d33 = 2.0 * self.q33 * p * EPS0 * self.epsilon_r;
1647 let s33 = 1.0 / self.elastic_modulus;
1648 let eps33 = EPS0 * self.epsilon_r;
1649 let k2 = (d33 * d33) / (s33 * eps33);
1650 k2.sqrt().clamp(0.0, 1.0)
1651 }
1652}
1653#[derive(Debug, Clone, Copy)]
1655pub struct PiezoActuator {
1656 pub stroke_per_volt: f64,
1658 pub force_per_volt: f64,
1660 pub stiffness: f64,
1662 pub resonance_freq: f64,
1664 pub capacitance: f64,
1666 pub hysteresis: f64,
1668}
1669impl PiezoActuator {
1670 pub fn new(
1672 stroke_per_volt: f64,
1673 force_per_volt: f64,
1674 stiffness: f64,
1675 resonance_freq: f64,
1676 capacitance: f64,
1677 ) -> Self {
1678 Self {
1679 stroke_per_volt,
1680 force_per_volt,
1681 stiffness,
1682 resonance_freq,
1683 capacitance,
1684 hysteresis: 0.1,
1685 }
1686 }
1687 pub fn free_stroke(&self, voltage: f64) -> f64 {
1689 self.stroke_per_volt * voltage
1690 }
1691 pub fn blocking_force(&self, voltage: f64) -> f64 {
1693 self.force_per_volt * voltage
1694 }
1695 pub fn output_force(&self, voltage: f64, displacement: f64) -> f64 {
1697 let free = self.free_stroke(voltage);
1698 self.stiffness * (free - displacement)
1699 }
1700 pub fn energy_per_cycle(&self, voltage: f64, freq: f64) -> f64 {
1702 let _ = freq;
1703 0.5 * self.capacitance * voltage * voltage
1704 }
1705 pub fn mechanical_work(&self, voltage: f64) -> f64 {
1707 let f = self.blocking_force(voltage);
1708 let d = self.free_stroke(voltage);
1709 0.5 * f * d
1710 }
1711}
1712#[derive(Debug, Clone)]
1717pub struct ThermochromicResponse {
1718 pub t_transition: f64,
1720 pub delta_t: f64,
1722 pub color_low: [f64; 3],
1724 pub color_high: [f64; 3],
1726 pub latent_heat: f64,
1728}
1729impl ThermochromicResponse {
1730 pub fn new(
1732 t_transition: f64,
1733 delta_t: f64,
1734 color_low: [f64; 3],
1735 color_high: [f64; 3],
1736 latent_heat: f64,
1737 ) -> Self {
1738 Self {
1739 t_transition,
1740 delta_t,
1741 color_low,
1742 color_high,
1743 latent_heat,
1744 }
1745 }
1746 pub fn transition_fraction(&self, temperature: f64) -> f64 {
1750 let x = (temperature - self.t_transition) / self.delta_t;
1751 1.0 / (1.0 + (-x).exp())
1752 }
1753 pub fn reflectance(&self, temperature: f64) -> [f64; 3] {
1755 let s = self.transition_fraction(temperature);
1756 [
1757 self.color_low[0] + s * (self.color_high[0] - self.color_low[0]),
1758 self.color_low[1] + s * (self.color_high[1] - self.color_low[1]),
1759 self.color_low[2] + s * (self.color_high[2] - self.color_low[2]),
1760 ]
1761 }
1762 pub fn luminance(&self, temperature: f64) -> f64 {
1764 let [r, g, b] = self.reflectance(temperature);
1765 0.2126 * r + 0.7152 * g + 0.0722 * b
1766 }
1767 pub fn effective_heat_capacity(&self, temperature: f64, c_base: f64) -> f64 {
1771 let s = self.transition_fraction(temperature);
1772 let ds_dt = s * (1.0 - s) / self.delta_t;
1773 c_base + self.latent_heat * ds_dt
1774 }
1775}
1776#[derive(Debug, Clone, Copy)]
1784#[allow(dead_code)]
1785pub struct PiezoelectricSmart {
1786 pub d33: f64,
1788 pub d31: f64,
1790 pub d15: f64,
1792 pub s33_e: f64,
1794 pub s11_e: f64,
1796 pub eps33_t: f64,
1798 pub density: f64,
1800 pub length: f64,
1802 pub area: f64,
1804}
1805impl PiezoelectricSmart {
1806 #[allow(clippy::too_many_arguments)]
1808 pub fn new(
1809 d33: f64,
1810 d31: f64,
1811 d15: f64,
1812 s33_e: f64,
1813 s11_e: f64,
1814 eps33_t: f64,
1815 density: f64,
1816 length: f64,
1817 area: f64,
1818 ) -> Self {
1819 Self {
1820 d33,
1821 d31,
1822 d15,
1823 s33_e,
1824 s11_e,
1825 eps33_t,
1826 density,
1827 length,
1828 area,
1829 }
1830 }
1831 pub fn pzt5h() -> Self {
1833 const EPS0: f64 = 8.854e-12;
1834 Self::new(
1835 593e-12,
1836 -274e-12,
1837 741e-12,
1838 20.7e-12,
1839 16.5e-12,
1840 3400.0 * EPS0,
1841 7500.0,
1842 10e-3,
1843 1e-4,
1844 )
1845 }
1846 pub fn pzt4() -> Self {
1848 const EPS0: f64 = 8.854e-12;
1849 Self::new(
1850 289e-12,
1851 -123e-12,
1852 496e-12,
1853 15.5e-12,
1854 12.3e-12,
1855 1300.0 * EPS0,
1856 7600.0,
1857 10e-3,
1858 1e-4,
1859 )
1860 }
1861 pub fn k33(&self) -> f64 {
1865 let k2 = self.d33 * self.d33 / (self.s33_e * self.eps33_t);
1866 k2.sqrt().clamp(0.0, 1.0)
1867 }
1868 pub fn kp(&self) -> f64 {
1870 let k31_2 = self.d31 * self.d31 / (self.s11_e * self.eps33_t);
1871 let kp2 = 2.0 * k31_2 / 0.7_f64;
1872 kp2.sqrt().clamp(0.0, 1.0)
1873 }
1874 pub fn resonance_frequency(&self) -> f64 {
1878 let v_sound = (1.0 / (self.density * self.s33_e)).sqrt();
1879 v_sound / (2.0 * self.length)
1880 }
1881 pub fn antiresonance_frequency(&self) -> f64 {
1883 let fr = self.resonance_frequency();
1884 let k = self.k33();
1885 fr * (1.0 / (1.0 - k * k)).sqrt()
1886 }
1887 pub fn actuation_stroke(&self, voltage: f64) -> f64 {
1891 self.d33 * voltage
1892 }
1893 pub fn actuation_stroke_31(&self, voltage: f64) -> f64 {
1895 let thickness = self.length;
1896 let width = self.area.sqrt();
1897 self.d31 * voltage / thickness * width
1898 }
1899 pub fn blocking_force(&self, voltage: f64) -> f64 {
1903 self.d33 * voltage * self.area / (self.s33_e * self.length)
1904 }
1905 pub fn charge_from_force(&self, force: f64) -> f64 {
1909 self.d33 * force
1910 }
1911 pub fn voltage_from_force(&self, force: f64) -> f64 {
1913 let stress = force / self.area;
1914 self.d33 * stress * self.length / self.eps33_t
1915 }
1916 pub fn mechanical_q(&self) -> f64 {
1920 let fr = self.resonance_frequency();
1921 let fa = self.antiresonance_frequency();
1922 if (fa - fr).abs() < 1e-3 {
1923 1000.0
1924 } else {
1925 fr / (fa - fr)
1926 }
1927 }
1928}
1929#[derive(Debug, Clone)]
1935pub struct SmaActuator {
1936 pub area: f64,
1938 pub length: f64,
1940 pub xi_s: f64,
1942 pub xi_t: f64,
1944 pub sma: ShapeMemoryAlloy,
1946 pub strain: f64,
1948 pub stress: f64,
1950}
1951impl SmaActuator {
1952 pub fn new(sma: ShapeMemoryAlloy, area: f64, length: f64) -> Self {
1954 Self {
1955 area,
1956 length,
1957 xi_s: 0.0,
1958 xi_t: sma.xi,
1959 sma,
1960 strain: 0.0,
1961 stress: 0.0,
1962 }
1963 }
1964 pub fn total_xi(&self) -> f64 {
1966 (self.xi_s + self.xi_t).clamp(0.0, 1.0)
1967 }
1968 pub fn recovery_stress(&self) -> f64 {
1972 let e = self.sma.e_a + self.total_xi() * (self.sma.e_m - self.sma.e_a);
1973 e * (self.strain - self.sma.max_strain * self.xi_s)
1974 }
1975 pub fn stroke(&self, xi_final: f64) -> f64 {
1979 let delta_xi = self.total_xi() - xi_final.clamp(0.0, 1.0);
1980 delta_xi * self.sma.max_strain * self.length
1981 }
1982 pub fn force(&self) -> f64 {
1984 self.recovery_stress() * self.area
1985 }
1986 pub fn update(&mut self, temperature: f64, applied_strain: f64) {
1988 self.strain = applied_strain;
1989 let xi_new = self.sma.update_phase(temperature, self.stress);
1990 let xi_s_new = (applied_strain / self.sma.max_strain.max(1e-12)).clamp(0.0, xi_new);
1991 self.xi_s = xi_s_new;
1992 self.xi_t = (xi_new - xi_s_new).max(0.0);
1993 self.stress = self.recovery_stress();
1994 }
1995}
1996#[derive(Debug, Clone, Copy)]
2001pub struct PiezoelectricCoupling {
2002 pub d33: f64,
2004 pub d31: f64,
2006 pub elastic_modulus_33: f64,
2008 pub elastic_modulus_11: f64,
2010 pub epsilon_r33: f64,
2012 pub k33: f64,
2014}
2015impl PiezoelectricCoupling {
2016 pub fn new(
2018 d33: f64,
2019 d31: f64,
2020 elastic_modulus_33: f64,
2021 elastic_modulus_11: f64,
2022 epsilon_r33: f64,
2023 ) -> Self {
2024 const EPS0: f64 = 8.854e-12;
2025 let k33 = d33 * (elastic_modulus_33 / (EPS0 * epsilon_r33)).sqrt();
2026 Self {
2027 d33,
2028 d31,
2029 elastic_modulus_33,
2030 elastic_modulus_11,
2031 epsilon_r33,
2032 k33,
2033 }
2034 }
2035 pub fn pzt5a() -> Self {
2037 Self::new(374e-12, -171e-12, 61e9, 61e9, 1700.0)
2038 }
2039 pub fn strain_from_voltage_33(&self, voltage: f64, thickness: f64) -> f64 {
2042 self.d33 * voltage / thickness
2043 }
2044 pub fn strain_from_voltage_31(&self, voltage: f64, thickness: f64) -> f64 {
2046 self.d31 * voltage / thickness
2047 }
2048 pub fn charge_from_stress_33(&self, stress: f64) -> f64 {
2050 self.d33 * stress
2051 }
2052 pub fn voltage_from_stress(&self, stress: f64, thickness: f64) -> f64 {
2054 const EPS0: f64 = 8.854e-12;
2055 let p = self.charge_from_stress_33(stress);
2056 p * thickness / (EPS0 * self.epsilon_r33)
2057 }
2058 pub fn harvested_energy_density(&self, stress_amplitude: f64) -> f64 {
2060 0.5 * self.d33 * self.d33 * stress_amplitude * stress_amplitude
2061 / (self.epsilon_r33 * 8.854e-12)
2062 }
2063}