use std::f64::consts::PI;
#[inline]
pub fn vec3_add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub fn vec3_sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub fn vec3_scale(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
pub fn vec3_dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub fn vec3_cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
#[inline]
pub fn vec3_norm(a: [f64; 3]) -> f64 {
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
}
#[inline]
pub fn vec3_normalize(a: [f64; 3]) -> [f64; 3] {
let n = vec3_norm(a);
if n < 1e-15 {
[0.0; 3]
} else {
vec3_scale(a, 1.0 / n)
}
}
pub fn dihedral_angle(e0: [f64; 3], e1: [f64; 3], p0: [f64; 3], p1: [f64; 3]) -> f64 {
let edge = vec3_sub(e1, e0);
let n0 = vec3_cross(vec3_sub(p0, e0), edge);
let n1 = vec3_cross(edge, vec3_sub(p1, e0));
let cos_a = vec3_dot(n0, n1) / (vec3_norm(n0) * vec3_norm(n1) + 1e-15);
PI - cos_a.clamp(-1.0, 1.0).acos()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreaseAssignment {
Mountain,
Valley,
Flat,
Border,
}
#[derive(Debug, Clone)]
pub struct Crease {
pub v0: usize,
pub v1: usize,
pub assignment: CreaseAssignment,
pub rest_angle: f64,
}
impl Crease {
pub fn new(v0: usize, v1: usize, assignment: CreaseAssignment, rest_angle: f64) -> Self {
Self {
v0,
v1,
assignment,
rest_angle,
}
}
}
#[derive(Debug, Clone)]
pub struct CreasePattern {
pub vertices: Vec<[f64; 2]>,
pub creases: Vec<Crease>,
}
impl CreasePattern {
pub fn new() -> Self {
Self {
vertices: Vec::new(),
creases: Vec::new(),
}
}
pub fn add_vertex(&mut self, x: f64, y: f64) -> usize {
let idx = self.vertices.len();
self.vertices.push([x, y]);
idx
}
pub fn add_crease(
&mut self,
v0: usize,
v1: usize,
assignment: CreaseAssignment,
rest_angle: f64,
) {
self.creases
.push(Crease::new(v0, v1, assignment, rest_angle));
}
pub fn count_mountain(&self) -> usize {
self.creases
.iter()
.filter(|c| c.assignment == CreaseAssignment::Mountain)
.count()
}
pub fn count_valley(&self) -> usize {
self.creases
.iter()
.filter(|c| c.assignment == CreaseAssignment::Valley)
.count()
}
pub fn check_maekawa(&self, vertex: usize) -> bool {
let mut m = 0i32;
let mut v = 0i32;
for c in &self.creases {
if c.v0 == vertex || c.v1 == vertex {
match c.assignment {
CreaseAssignment::Mountain => m += 1,
CreaseAssignment::Valley => v += 1,
_ => {}
}
}
}
(m - v).abs() == 2
}
}
impl Default for CreasePattern {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct MiuraOriUnit {
pub a: f64,
pub b: f64,
pub gamma: f64,
pub fold_angle: f64,
}
impl MiuraOriUnit {
pub fn new(a: f64, b: f64, gamma: f64) -> Self {
Self {
a,
b,
gamma,
fold_angle: 0.0,
}
}
pub fn projected_width(&self) -> f64 {
self.a * self.fold_angle.sin() * self.gamma.sin()
}
pub fn projected_height(&self) -> f64 {
self.b * self.fold_angle.cos().abs()
}
pub fn poisson_ratio(&self) -> f64 {
let s = self.fold_angle.sin();
let c = self.fold_angle.cos();
if s.abs() < 1e-10 || c.abs() < 1e-10 {
return f64::NAN;
}
(c * c) / (s * s)
}
pub fn fold_step(&mut self, delta_theta: f64) {
self.fold_angle = (self.fold_angle + delta_theta).clamp(0.0, PI);
}
}
#[derive(Debug, Clone)]
pub struct MiuraOriSheet {
pub nx: usize,
pub ny: usize,
pub unit: MiuraOriUnit,
}
impl MiuraOriSheet {
pub fn new(nx: usize, ny: usize, a: f64, b: f64, gamma: f64) -> Self {
Self {
nx,
ny,
unit: MiuraOriUnit::new(a, b, gamma),
}
}
pub fn total_width(&self) -> f64 {
self.nx as f64 * self.unit.projected_width()
}
pub fn total_height(&self) -> f64 {
self.ny as f64 * self.unit.projected_height()
}
pub fn set_fold_angle(&mut self, theta: f64) {
self.unit.fold_angle = theta.clamp(0.0, PI);
}
pub fn fold_step(&mut self, delta_theta: f64) {
self.unit.fold_step(delta_theta);
}
}
#[derive(Debug, Clone)]
pub struct RigidPanel {
pub id: usize,
pub vertices: Vec<[f64; 3]>,
pub rotation: [[f64; 3]; 3],
pub translation: [f64; 3],
}
impl RigidPanel {
pub fn new(id: usize, vertices: Vec<[f64; 3]>) -> Self {
let rot = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
Self {
id,
vertices,
rotation: rot,
translation: [0.0; 3],
}
}
pub fn centroid(&self) -> [f64; 3] {
let n = self.vertices.len() as f64;
let mut c = [0.0f64; 3];
for v in &self.vertices {
c[0] += v[0];
c[1] += v[1];
c[2] += v[2];
}
[c[0] / n, c[1] / n, c[2] / n]
}
pub fn apply_rotation(&mut self, r: [[f64; 3]; 3]) {
let cen = self.centroid();
for v in &mut self.vertices {
let local = vec3_sub(*v, cen);
let rotated = mat3_mul_vec(r, local);
*v = vec3_add(rotated, cen);
}
}
}
fn mat3_mul_vec(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
pub fn rotation_matrix_axis_angle(axis: [f64; 3], theta: f64) -> [[f64; 3]; 3] {
let [kx, ky, kz] = vec3_normalize(axis);
let c = theta.cos();
let s = theta.sin();
let t = 1.0 - c;
[
[t * kx * kx + c, t * kx * ky - s * kz, t * kx * kz + s * ky],
[t * kx * ky + s * kz, t * ky * ky + c, t * ky * kz - s * kx],
[t * kx * kz - s * ky, t * ky * kz + s * kx, t * kz * kz + c],
]
}
#[derive(Debug, Clone)]
pub struct FoldHinge {
pub panel_a: usize,
pub panel_b: usize,
pub axis: [f64; 3],
pub angle: f64,
pub rest_angle: f64,
pub stiffness: f64,
}
impl FoldHinge {
pub fn new(
panel_a: usize,
panel_b: usize,
axis: [f64; 3],
rest_angle: f64,
stiffness: f64,
) -> Self {
Self {
panel_a,
panel_b,
axis: vec3_normalize(axis),
angle: 0.0,
rest_angle,
stiffness,
}
}
pub fn restoring_torque(&self) -> f64 {
self.stiffness * (self.rest_angle - self.angle)
}
pub fn rotate(&mut self, delta: f64) {
self.angle += delta;
}
}
#[derive(Debug, Clone)]
pub struct CompliantHinge {
pub k_torsion: f64,
pub c_damp: f64,
pub angle: f64,
pub angular_velocity: f64,
pub rest_angle: f64,
}
impl CompliantHinge {
pub fn new(k_torsion: f64, c_damp: f64, rest_angle: f64) -> Self {
Self {
k_torsion,
c_damp,
angle: 0.0,
angular_velocity: 0.0,
rest_angle,
}
}
pub fn torque_elastic(&self) -> f64 {
-self.k_torsion * (self.angle - self.rest_angle)
}
pub fn torque_damping(&self) -> f64 {
-self.c_damp * self.angular_velocity
}
pub fn net_torque(&self) -> f64 {
self.torque_elastic() + self.torque_damping()
}
pub fn step(&mut self, dt: f64, inertia: f64, external_torque: f64) {
let total_torque = self.net_torque() + external_torque;
let alpha = total_torque / (inertia + 1e-15);
self.angular_velocity += alpha * dt;
self.angle += self.angular_velocity * dt;
}
}
#[derive(Debug, Clone)]
pub struct WaterbombBase {
pub half_size: f64,
pub fold_angle: f64,
}
impl WaterbombBase {
pub fn new(side: f64) -> Self {
Self {
half_size: side * 0.5,
fold_angle: 0.0,
}
}
pub fn apex_height(&self) -> f64 {
self.half_size * self.fold_angle.sin()
}
pub fn footprint_radius(&self) -> f64 {
self.half_size * self.fold_angle.cos()
}
pub fn set_fold_angle(&mut self, theta: f64) {
self.fold_angle = theta.clamp(0.0, PI / 2.0);
}
pub fn fold_fraction(&self) -> f64 {
self.fold_angle / (PI / 2.0)
}
}
#[derive(Debug, Clone)]
pub struct YoshimuraPattern {
pub radius: f64,
pub height: f64,
pub n_cols: usize,
pub n_rows: usize,
pub compression: f64,
}
impl YoshimuraPattern {
pub fn new(radius: f64, height: f64, n_cols: usize, n_rows: usize) -> Self {
Self {
radius,
height,
n_cols,
n_rows,
compression: 0.0,
}
}
pub fn compressed_height(&self) -> f64 {
self.height * (1.0 - self.compression)
}
pub fn diamond_half_angle(&self) -> f64 {
PI / self.n_cols as f64
}
pub fn compressive_strain(&self) -> f64 {
self.compression
}
pub fn compress(&mut self, delta: f64) {
self.compression = (self.compression + delta).clamp(0.0, 0.999);
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DeployState {
Stowed,
Deploying,
Deployed,
}
#[derive(Debug, Clone)]
pub struct DeployableStructure {
pub panels: Vec<RigidPanel>,
pub hinges: Vec<FoldHinge>,
pub state: DeployState,
pub progress: f64,
}
impl DeployableStructure {
pub fn new(panels: Vec<RigidPanel>, hinges: Vec<FoldHinge>) -> Self {
Self {
panels,
hinges,
state: DeployState::Stowed,
progress: 0.0,
}
}
pub fn deploy_step(&mut self, step: f64) {
self.progress = (self.progress + step).clamp(0.0, 1.0);
for hinge in &mut self.hinges {
hinge.angle = hinge.rest_angle * self.progress;
}
self.state = if self.progress >= 1.0 - 1e-6 {
DeployState::Deployed
} else if self.progress <= 1e-6 {
DeployState::Stowed
} else {
DeployState::Deploying
};
}
pub fn reset(&mut self) {
self.progress = 0.0;
for hinge in &mut self.hinges {
hinge.angle = 0.0;
}
self.state = DeployState::Stowed;
}
pub fn is_fully_deployed(&self) -> bool {
self.hinges
.iter()
.all(|h| (h.angle - h.rest_angle).abs() < 1e-3)
}
}
#[derive(Debug, Clone)]
pub struct BistableOrigami {
pub k_snap: f64,
pub theta_c: f64,
pub angle: f64,
pub angular_velocity: f64,
}
impl BistableOrigami {
pub fn new(k_snap: f64, theta_c: f64) -> Self {
Self {
k_snap,
theta_c,
angle: -theta_c * 0.99,
angular_velocity: 0.0,
}
}
pub fn potential_energy(&self) -> f64 {
let x = self.angle * self.angle - self.theta_c * self.theta_c;
self.k_snap * x * x
}
pub fn restoring_torque(&self) -> f64 {
-4.0 * self.k_snap * self.angle * (self.angle * self.angle - self.theta_c * self.theta_c)
}
pub fn stable_state(&self) -> i32 {
if self.angle > self.theta_c * 0.1 {
1
} else if self.angle < -self.theta_c * 0.1 {
-1
} else {
0
}
}
pub fn step(&mut self, dt: f64, inertia: f64, external_torque: f64) {
let torque = self.restoring_torque() + external_torque;
let alpha = torque / (inertia + 1e-15);
self.angular_velocity += alpha * dt;
self.angle += self.angular_velocity * dt;
}
}
pub fn miura_longitudinal_stiffness(
t: f64,
e_mat: f64,
a: f64,
b: f64,
gamma: f64,
theta: f64,
) -> f64 {
let f_geom = gamma.sin().powi(2) * theta.cos().powi(2)
/ (a * b
* (1.0 + theta.sin().powi(2) * gamma.cos().powi(2) / gamma.sin().powi(2)).max(1e-10));
e_mat * t.powi(3) * f_geom
}
pub fn miura_transverse_stiffness(
t: f64,
e_mat: f64,
a: f64,
b: f64,
gamma: f64,
theta: f64,
) -> f64 {
let f_geom = gamma.cos().powi(2) * theta.sin().powi(2) / (a * b + 1e-15);
e_mat * t.powi(3) * f_geom
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SmaPhase {
Martensite,
Austenite,
}
#[derive(Debug, Clone)]
pub struct SmaActuator {
pub area: f64,
pub l_martensite: f64,
pub l_austenite: f64,
pub t_af: f64,
pub t_mf: f64,
pub temperature: f64,
pub phase: SmaPhase,
pub recovery_stress: f64,
}
impl SmaActuator {
pub fn new(
area: f64,
l_martensite: f64,
l_austenite: f64,
t_af: f64,
t_mf: f64,
recovery_stress: f64,
) -> Self {
Self {
area,
l_martensite,
l_austenite,
t_af,
t_mf,
temperature: t_mf - 10.0,
phase: SmaPhase::Martensite,
recovery_stress,
}
}
pub fn update_phase(&mut self) {
if self.phase == SmaPhase::Martensite && self.temperature >= self.t_af {
self.phase = SmaPhase::Austenite;
} else if self.phase == SmaPhase::Austenite && self.temperature <= self.t_mf {
self.phase = SmaPhase::Martensite;
}
}
pub fn current_length(&self) -> f64 {
match self.phase {
SmaPhase::Martensite => self.l_martensite,
SmaPhase::Austenite => {
let frac = ((self.temperature - self.t_mf) / (self.t_af - self.t_mf + 1e-15))
.clamp(0.0, 1.0);
self.l_martensite + frac * (self.l_austenite - self.l_martensite)
}
}
}
pub fn actuation_force(&self) -> f64 {
if self.phase == SmaPhase::Austenite {
self.recovery_stress * self.area
} else {
0.0
}
}
pub fn heat(&mut self, delta_t: f64) {
self.temperature += delta_t;
self.update_phase();
}
pub fn cool(&mut self, delta_t: f64) {
self.temperature -= delta_t;
self.update_phase();
}
pub fn contraction_ratio(&self) -> f64 {
let contraction = self.l_martensite - self.current_length();
(contraction / (self.l_martensite - self.l_austenite + 1e-15)).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct SmaFoldSystem {
pub hinge: CompliantHinge,
pub actuator: SmaActuator,
pub mechanical_advantage: f64,
}
impl SmaFoldSystem {
pub fn new(hinge: CompliantHinge, actuator: SmaActuator, mechanical_advantage: f64) -> Self {
Self {
hinge,
actuator,
mechanical_advantage,
}
}
pub fn sma_torque(&self) -> f64 {
self.actuator.actuation_force() * self.mechanical_advantage
}
pub fn step(&mut self, dt: f64, inertia: f64) {
let external_torque = self.sma_torque();
self.hinge.step(dt, inertia, external_torque);
}
pub fn actuate(&mut self, delta_t: f64) {
self.actuator.heat(delta_t);
}
pub fn deactivate(&mut self, delta_t: f64) {
self.actuator.cool(delta_t);
}
pub fn fold_angle(&self) -> f64 {
self.hinge.angle
}
}
pub fn snap_through_force(k: f64, x: f64, x_c: f64) -> f64 {
-k * x * (x * x - x_c * x_c)
}
pub fn snap_through_critical_force(k: f64, x_c: f64) -> f64 {
k * (2.0 / (3.0 * 3.0_f64.sqrt())) * x_c.powi(3)
}
pub fn degree4_vertex_fold_angles(rho0: f64, alpha: [f64; 4]) -> [f64; 4] {
let ta = rho0.tan() * 0.5;
let r = (alpha[0] / 2.0).cos() / ((alpha[0] / 2.0).sin() + 1e-15) * (alpha[2] / 2.0).sin()
/ ((alpha[2] / 2.0).cos() + 1e-15);
let rho1 = 2.0 * (ta * r).atan();
let rho2 = rho0; let rho3 = rho1;
[rho0, rho1, rho2, rho3]
}
pub fn panel_normal(v: &[[f64; 3]]) -> [f64; 3] {
if v.len() < 3 {
return [0.0, 0.0, 1.0];
}
let e1 = vec3_sub(v[1], v[0]);
let e2 = vec3_sub(v[2], v[0]);
vec3_normalize(vec3_cross(e1, e2))
}
pub fn total_fold_energy(hinges: &[FoldHinge]) -> f64 {
hinges
.iter()
.map(|h| 0.5 * h.stiffness * (h.angle - h.rest_angle).powi(2))
.sum()
}
pub fn total_compliant_hinge_energy(hinges: &[CompliantHinge]) -> f64 {
hinges
.iter()
.map(|h| 0.5 * h.k_torsion * (h.angle - h.rest_angle).powi(2))
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_vec3_add() {
let r = vec3_add([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]);
assert!((r[0] - 5.0).abs() < EPS);
assert!((r[1] - 7.0).abs() < EPS);
assert!((r[2] - 9.0).abs() < EPS);
}
#[test]
fn test_vec3_cross_orthogonal() {
let x = [1.0, 0.0, 0.0];
let y = [0.0, 1.0, 0.0];
let z = vec3_cross(x, y);
assert!((z[0]).abs() < EPS);
assert!((z[1]).abs() < EPS);
assert!((z[2] - 1.0).abs() < EPS);
}
#[test]
fn test_vec3_normalize_unit() {
let v = vec3_normalize([3.0, 4.0, 0.0]);
assert!((vec3_norm(v) - 1.0).abs() < EPS);
}
#[test]
fn test_vec3_normalize_zero() {
let v = vec3_normalize([0.0, 0.0, 0.0]);
assert_eq!(v, [0.0, 0.0, 0.0]);
}
#[test]
fn test_dihedral_angle_flat() {
let angle = dihedral_angle(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[0.5, -1.0, 0.0],
);
assert!(
(angle - PI).abs() < 1e-6,
"flat dihedral should be π, got {angle}"
);
}
#[test]
fn test_dihedral_angle_perpendicular() {
let angle = dihedral_angle(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.5, 1.0, 0.0],
[0.5, 0.0, 1.0],
);
assert!(
(angle - PI / 2.0).abs() < 1e-6,
"perpendicular angle should be π/2, got {angle}"
);
}
#[test]
fn test_maekawa_theorem() {
let mut cp = CreasePattern::new();
let center = cp.add_vertex(0.0, 0.0);
let _v1 = cp.add_vertex(1.0, 0.0);
let _v2 = cp.add_vertex(0.0, 1.0);
let _v3 = cp.add_vertex(-1.0, 0.0);
let _v4 = cp.add_vertex(0.0, -1.0);
cp.add_crease(center, 1, CreaseAssignment::Mountain, PI);
cp.add_crease(center, 2, CreaseAssignment::Mountain, PI);
cp.add_crease(center, 3, CreaseAssignment::Mountain, PI);
cp.add_crease(center, 4, CreaseAssignment::Valley, 0.0);
assert!(
cp.check_maekawa(center),
"Maekawa theorem should hold for 3M+1V"
);
}
#[test]
fn test_crease_counts() {
let mut cp = CreasePattern::new();
let a = cp.add_vertex(0.0, 0.0);
let b = cp.add_vertex(1.0, 0.0);
let c = cp.add_vertex(0.0, 1.0);
cp.add_crease(a, b, CreaseAssignment::Mountain, PI);
cp.add_crease(a, c, CreaseAssignment::Valley, 0.0);
cp.add_crease(b, c, CreaseAssignment::Mountain, PI);
assert_eq!(cp.count_mountain(), 2);
assert_eq!(cp.count_valley(), 1);
}
#[test]
fn test_miura_projected_dimensions() {
let unit = MiuraOriUnit::new(1.0, 1.0, PI / 4.0);
assert!(unit.projected_width().abs() < EPS);
assert!((unit.projected_height() - 1.0).abs() < EPS);
}
#[test]
fn test_miura_fold_step() {
let mut unit = MiuraOriUnit::new(1.0, 1.0, PI / 4.0);
unit.fold_step(PI / 4.0);
assert!(unit.projected_width() > 0.0);
assert!(unit.projected_height() < 1.0);
}
#[test]
fn test_miura_sheet_dimensions() {
let mut sheet = MiuraOriSheet::new(3, 4, 1.0, 1.0, PI / 4.0);
sheet.set_fold_angle(PI / 4.0);
let w1 = sheet.total_width();
let h1 = sheet.total_height();
let mut sheet2 = MiuraOriSheet::new(6, 4, 1.0, 1.0, PI / 4.0);
sheet2.set_fold_angle(PI / 4.0);
assert!((sheet2.total_width() - 2.0 * w1).abs() < 1e-10);
assert!((sheet2.total_height() - h1).abs() < 1e-10);
}
#[test]
fn test_rotation_matrix_z90() {
let r = rotation_matrix_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
let x_rot = mat3_mul_vec(r, [1.0, 0.0, 0.0]);
assert!((x_rot[0]).abs() < 1e-10);
assert!((x_rot[1] - 1.0).abs() < 1e-10);
assert!((x_rot[2]).abs() < 1e-10);
}
#[test]
fn test_fold_hinge_restoring_torque() {
let hinge = FoldHinge::new(0, 1, [0.0, 0.0, 1.0], PI / 2.0, 10.0);
let expected = 10.0 * PI / 2.0;
assert!((hinge.restoring_torque() - expected).abs() < 1e-10);
}
#[test]
fn test_compliant_hinge_step() {
let mut hinge = CompliantHinge::new(1.0, 0.0, PI / 4.0);
for _ in 0..1000 {
hinge.step(0.01, 0.001, 0.0);
}
assert!(
hinge.angle > 0.0,
"hinge should have moved toward positive rest angle"
);
}
#[test]
fn test_waterbomb_apex_height() {
let mut wb = WaterbombBase::new(2.0);
wb.set_fold_angle(PI / 2.0);
assert!((wb.apex_height() - 1.0).abs() < EPS);
}
#[test]
fn test_waterbomb_footprint() {
let mut wb = WaterbombBase::new(2.0);
wb.set_fold_angle(PI / 4.0);
let expected = 1.0 * (PI / 4.0).cos();
assert!((wb.footprint_radius() - expected).abs() < EPS);
}
#[test]
fn test_yoshimura_compression() {
let mut yp = YoshimuraPattern::new(0.1, 1.0, 8, 4);
yp.compress(0.3);
assert!((yp.compressed_height() - 0.7).abs() < EPS);
}
#[test]
fn test_deployable_deploy_step() {
let panels = vec![RigidPanel::new(0, vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]])];
let hinges = vec![FoldHinge::new(0, 0, [0.0, 0.0, 1.0], PI / 2.0, 1.0)];
let mut ds = DeployableStructure::new(panels, hinges);
ds.deploy_step(0.5);
assert_eq!(ds.state, DeployState::Deploying);
assert!((ds.progress - 0.5).abs() < EPS);
ds.deploy_step(0.5);
assert_eq!(ds.state, DeployState::Deployed);
}
#[test]
fn test_deployable_reset() {
let panels = vec![];
let hinges = vec![FoldHinge::new(0, 1, [1.0, 0.0, 0.0], PI, 1.0)];
let mut ds = DeployableStructure::new(panels, hinges);
ds.deploy_step(1.0);
ds.reset();
assert_eq!(ds.state, DeployState::Stowed);
assert!(ds.progress.abs() < EPS);
}
#[test]
fn test_bistable_potential_wells() {
let bs = BistableOrigami::new(1.0, 1.0);
let mut bs_pos = bs.clone();
bs_pos.angle = 1.0;
assert!(bs_pos.potential_energy().abs() < 1e-10);
let mut bs_neg = bs.clone();
bs_neg.angle = -1.0;
assert!(bs_neg.potential_energy().abs() < 1e-10);
}
#[test]
fn test_bistable_stable_states() {
let mut bs = BistableOrigami::new(1.0, 1.0);
bs.angle = -0.99;
assert_eq!(bs.stable_state(), -1);
bs.angle = 0.99;
assert_eq!(bs.stable_state(), 1);
}
#[test]
fn test_bistable_step_integration() {
let mut bs = BistableOrigami::new(1.0, 1.0);
bs.angle = 1.5; let initial_v = bs.angular_velocity;
bs.step(0.001, 0.01, 0.0);
assert!(bs.angular_velocity != initial_v);
}
#[test]
fn test_sma_phase_transition() {
let mut sma = SmaActuator::new(1e-6, 0.1, 0.09, 350.0, 300.0, 200e6);
assert_eq!(sma.phase, SmaPhase::Martensite);
sma.heat(60.0); assert_eq!(sma.phase, SmaPhase::Austenite);
}
#[test]
fn test_sma_contraction_ratio() {
let mut sma = SmaActuator::new(1e-6, 0.1, 0.09, 350.0, 300.0, 200e6);
sma.heat(60.0); let cr = sma.contraction_ratio();
assert!(cr > 0.0 && cr <= 1.0);
}
#[test]
fn test_sma_actuation_force() {
let mut sma = SmaActuator::new(1e-6, 0.1, 0.09, 350.0, 300.0, 200e6);
assert!(sma.actuation_force().abs() < EPS, "no force in martensite");
sma.heat(60.0);
assert!(sma.actuation_force() > 0.0, "force in austenite");
}
#[test]
fn test_sma_fold_system_activation() {
let hinge = CompliantHinge::new(0.01, 0.001, PI / 2.0);
let sma = SmaActuator::new(1e-5, 0.1, 0.08, 350.0, 300.0, 200e6);
let mut sys = SmaFoldSystem::new(hinge, sma, 0.01);
sys.actuate(60.0); let initial_angle = sys.fold_angle();
for _ in 0..500 {
sys.step(0.001, 0.001);
}
assert!(
sys.fold_angle() > initial_angle,
"fold should increase under SMA actuation"
);
}
#[test]
fn test_miura_stiffness_positive() {
let s = miura_longitudinal_stiffness(0.001, 70e9, 0.05, 0.05, PI / 4.0, PI / 4.0);
assert!(s > 0.0, "stiffness should be positive, got {s}");
}
#[test]
fn test_snap_through_zero_at_equilibria() {
let k = 1.0;
let x_c = 1.0;
assert!(snap_through_force(k, 0.0, x_c).abs() < EPS);
assert!(snap_through_force(k, x_c, x_c).abs() < EPS);
assert!(snap_through_force(k, -x_c, x_c).abs() < EPS);
}
#[test]
fn test_total_fold_energy() {
let hinges = vec![
FoldHinge {
panel_a: 0,
panel_b: 1,
axis: [1.0, 0.0, 0.0],
angle: 1.0,
rest_angle: 0.0,
stiffness: 2.0,
},
FoldHinge {
panel_a: 1,
panel_b: 2,
axis: [0.0, 1.0, 0.0],
angle: 0.5,
rest_angle: 0.0,
stiffness: 4.0,
},
];
assert!((total_fold_energy(&hinges) - 1.5).abs() < EPS);
}
#[test]
fn test_panel_normal_xy_plane() {
let verts = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let n = panel_normal(&verts);
assert!(n[0].abs() < EPS);
assert!(n[1].abs() < EPS);
assert!((n[2].abs() - 1.0).abs() < EPS);
}
#[test]
fn test_rigid_panel_centroid() {
let panel = RigidPanel::new(
0,
vec![
[0.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[2.0, 2.0, 0.0],
[0.0, 2.0, 0.0],
],
);
let c = panel.centroid();
assert!((c[0] - 1.0).abs() < EPS);
assert!((c[1] - 1.0).abs() < EPS);
}
#[test]
fn test_degree4_vertex_fold() {
let alpha = [PI / 4.0; 4];
let angles = degree4_vertex_fold_angles(PI / 6.0, alpha);
assert!((angles[0] - angles[2]).abs() < EPS);
assert!((angles[1] - angles[3]).abs() < EPS);
}
#[test]
fn test_miura_poisson_ratio() {
let mut unit = MiuraOriUnit::new(1.0, 1.0, PI / 4.0);
unit.fold_angle = PI / 4.0;
let nu = unit.poisson_ratio();
assert!(nu > 0.0, "poisson ratio should be positive, got {nu}");
}
#[test]
fn test_total_compliant_hinge_energy() {
let hinges = vec![
CompliantHinge {
k_torsion: 10.0,
c_damp: 0.1,
angle: 1.0,
angular_velocity: 0.0,
rest_angle: 0.0,
},
CompliantHinge {
k_torsion: 5.0,
c_damp: 0.1,
angle: 2.0,
angular_velocity: 0.0,
rest_angle: 0.0,
},
];
assert!((total_compliant_hinge_energy(&hinges) - 15.0).abs() < EPS);
}
#[test]
fn test_snap_through_critical_force() {
let f = snap_through_critical_force(1.0, 1.0);
assert!(f > 0.0, "critical force should be positive, got {f}");
}
}