1use std::f64::consts::PI;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct V3 {
25 pub x: f64,
27 pub y: f64,
29 pub z: f64,
31}
32
33impl V3 {
34 pub fn new(x: f64, y: f64, z: f64) -> Self {
36 Self { x, y, z }
37 }
38
39 pub fn zero() -> Self {
41 Self::new(0.0, 0.0, 0.0)
42 }
43
44 pub fn norm(&self) -> f64 {
46 (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
47 }
48
49 pub fn normalize(&self) -> Self {
51 let n = self.norm();
52 if n < 1e-15 {
53 Self::zero()
54 } else {
55 Self::new(self.x / n, self.y / n, self.z / n)
56 }
57 }
58
59 pub fn dot(&self, other: &Self) -> f64 {
61 self.x * other.x + self.y * other.y + self.z * other.z
62 }
63
64 pub fn cross(&self, other: &Self) -> Self {
66 Self::new(
67 self.y * other.z - self.z * other.y,
68 self.z * other.x - self.x * other.z,
69 self.x * other.y - self.y * other.x,
70 )
71 }
72
73 pub fn add(&self, other: &Self) -> Self {
75 Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
76 }
77
78 pub fn sub(&self, other: &Self) -> Self {
80 Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
81 }
82
83 pub fn scale(&self, s: f64) -> Self {
85 Self::new(self.x * s, self.y * s, self.z * s)
86 }
87}
88
89#[derive(Debug, Clone)]
100pub struct GridShell {
101 pub rows: usize,
103 pub cols: usize,
105 pub nodes: Vec<V3>,
107 pub members: Vec<(usize, usize)>,
109 pub rest_lengths: Vec<f64>,
111 pub stiffnesses: Vec<f64>,
113 pub fixed_nodes: Vec<usize>,
115 pub nodal_mass: Vec<f64>,
117}
118
119impl GridShell {
120 pub fn parabolic(rows: usize, cols: usize, span_x: f64, span_y: f64, sag: f64) -> Self {
126 assert!(rows >= 2 && cols >= 2);
127 let mut nodes = Vec::with_capacity(rows * cols);
128 for i in 0..rows {
129 let u = i as f64 / (rows - 1) as f64; for j in 0..cols {
131 let v = j as f64 / (cols - 1) as f64;
132 let x = u * span_x;
133 let y = v * span_y;
134 let z = 4.0 * sag * u * (1.0 - u) * v * (1.0 - v);
136 nodes.push(V3::new(x, y, z));
137 }
138 }
139 let mut members = Vec::new();
140 for i in 0..rows {
142 for j in 0..cols {
143 let idx = i * cols + j;
144 if j + 1 < cols {
145 members.push((idx, idx + 1));
146 }
147 if i + 1 < rows {
148 members.push((idx, idx + cols));
149 }
150 }
151 }
152 for i in 0..rows - 1 {
154 for j in 0..cols - 1 {
155 let a = i * cols + j;
156 let b = (i + 1) * cols + (j + 1);
157 members.push((a, b));
158 let c = i * cols + (j + 1);
159 let d = (i + 1) * cols + j;
160 members.push((c, d));
161 }
162 }
163 let rest_lengths: Vec<f64> = members
164 .iter()
165 .map(|(a, b)| nodes[*a].sub(&nodes[*b]).norm())
166 .collect();
167 let stiffnesses = vec![1.0e6_f64; members.len()]; let nodal_mass = vec![10.0_f64; nodes.len()];
169 let mut fixed_nodes = Vec::new();
171 for i in 0..rows {
172 for j in 0..cols {
173 if i == 0 || i == rows - 1 || j == 0 || j == cols - 1 {
174 fixed_nodes.push(i * cols + j);
175 }
176 }
177 }
178 Self {
179 rows,
180 cols,
181 nodes,
182 members,
183 rest_lengths,
184 stiffnesses,
185 fixed_nodes,
186 nodal_mass,
187 }
188 }
189
190 pub fn dynamic_relaxation(&mut self, max_iter: usize, dt: f64, damping: f64, tol: f64) -> f64 {
195 let n = self.nodes.len();
196 let fixed: std::collections::HashSet<usize> = self.fixed_nodes.iter().copied().collect();
197 let mut velocities = vec![V3::zero(); n];
198 let mut max_residual = f64::MAX;
199
200 for _iter in 0..max_iter {
201 let mut forces = vec![V3::zero(); n];
202
203 for (m_idx, (a, b)) in self.members.iter().enumerate() {
205 let pa = &self.nodes[*a];
206 let pb = &self.nodes[*b];
207 let delta = pb.sub(pa);
208 let length = delta.norm();
209 if length < 1e-15 {
210 continue;
211 }
212 let strain = (length - self.rest_lengths[m_idx]) / self.rest_lengths[m_idx];
213 let f_mag = self.stiffnesses[m_idx] * strain;
214 let dir = delta.normalize();
215 let force = dir.scale(f_mag);
216 forces[*a] = forces[*a].add(&force);
217 forces[*b] = forces[*b].sub(&force);
218 }
219
220 max_residual = 0.0;
222 for i in 0..n {
223 if fixed.contains(&i) {
224 velocities[i] = V3::zero();
225 continue;
226 }
227 let f_norm = forces[i].norm();
228 if f_norm > max_residual {
229 max_residual = f_norm;
230 }
231 let acc = forces[i].scale(1.0 / self.nodal_mass[i]);
233 velocities[i] = velocities[i].scale(1.0 - damping).add(&acc.scale(dt));
234 self.nodes[i] = self.nodes[i].add(&velocities[i].scale(dt));
235 }
236
237 if max_residual < tol {
238 break;
239 }
240 }
241 max_residual
242 }
243
244 pub fn max_member_force(&self) -> f64 {
246 self.members
247 .iter()
248 .enumerate()
249 .map(|(m_idx, (a, b))| {
250 let length = self.nodes[*a].sub(&self.nodes[*b]).norm();
251 let strain = (length - self.rest_lengths[m_idx]) / self.rest_lengths[m_idx];
252 (self.stiffnesses[m_idx] * strain).abs()
253 })
254 .fold(0.0_f64, f64::max)
255 }
256
257 pub fn node_count(&self) -> usize {
259 self.nodes.len()
260 }
261
262 pub fn member_count(&self) -> usize {
264 self.members.len()
265 }
266}
267
268#[derive(Debug, Clone)]
277pub struct PanelizationResult {
278 pub quad_panels: Vec<[usize; 4]>,
280 pub tri_panels: Vec<[usize; 3]>,
282 pub vertices: Vec<V3>,
284 pub planarity_errors: Vec<f64>,
287 pub planarity_tolerance: f64,
289 pub gap_stats: (f64, f64),
291}
292
293impl PanelizationResult {
294 pub fn quad_planarity_error(pts: &[V3; 4]) -> f64 {
299 let ab = pts[1].sub(&pts[0]);
300 let ac = pts[2].sub(&pts[0]);
301 let normal = ab.cross(&ac);
302 let n_len = normal.norm();
303 if n_len < 1e-15 {
304 return 0.0;
305 }
306 let n_unit = normal.normalize();
307 let ad = pts[3].sub(&pts[0]);
308 ad.dot(&n_unit).abs()
309 }
310
311 pub fn out_of_tolerance_count(&self) -> usize {
313 self.planarity_errors
314 .iter()
315 .filter(|&&e| e > self.planarity_tolerance)
316 .count()
317 }
318
319 pub fn mean_planarity_error(&self) -> f64 {
321 if self.planarity_errors.is_empty() {
322 return 0.0;
323 }
324 let sum: f64 = self.planarity_errors.iter().sum();
325 sum / self.planarity_errors.len() as f64
326 }
327
328 pub fn max_planarity_error(&self) -> f64 {
330 self.planarity_errors
331 .iter()
332 .cloned()
333 .fold(0.0_f64, f64::max)
334 }
335}
336
337#[derive(Debug, Clone)]
346pub struct PlanarQuadMesh {
347 pub nu: usize,
349 pub nv: usize,
351 pub vertices: Vec<V3>,
353 pub faces: Vec<[usize; 4]>,
355 pub planarity_errors: Vec<f64>,
357}
358
359impl PlanarQuadMesh {
360 pub fn from_surface<F>(nu: usize, nv: usize, surface_fn: F) -> Self
365 where
366 F: Fn(f64, f64) -> V3,
367 {
368 assert!(nu >= 2 && nv >= 2);
369 let mut vertices = Vec::with_capacity(nu * nv);
370 for i in 0..nu {
371 let u = i as f64 / (nu - 1) as f64;
372 for j in 0..nv {
373 let v = j as f64 / (nv - 1) as f64;
374 vertices.push(surface_fn(u, v));
375 }
376 }
377 let mut faces = Vec::new();
378 for i in 0..nu - 1 {
379 for j in 0..nv - 1 {
380 let a = i * nv + j;
381 let b = i * nv + j + 1;
382 let c = (i + 1) * nv + j + 1;
383 let d = (i + 1) * nv + j;
384 faces.push([a, b, c, d]);
385 }
386 }
387 let planarity_errors: Vec<f64> = faces
388 .iter()
389 .map(|[a, b, c, d]| {
390 let pts = [vertices[*a], vertices[*b], vertices[*c], vertices[*d]];
391 PanelizationResult::quad_planarity_error(&pts)
392 })
393 .collect();
394 Self {
395 nu,
396 nv,
397 vertices,
398 faces,
399 planarity_errors,
400 }
401 }
402
403 pub fn mean_planarity_error(&self) -> f64 {
405 if self.planarity_errors.is_empty() {
406 return 0.0;
407 }
408 self.planarity_errors.iter().sum::<f64>() / self.planarity_errors.len() as f64
409 }
410
411 pub fn max_planarity_error(&self) -> f64 {
413 self.planarity_errors
414 .iter()
415 .cloned()
416 .fold(0.0_f64, f64::max)
417 }
418
419 pub fn planarity_compliance_ratio(&self, tol: f64) -> f64 {
421 let compliant = self.planarity_errors.iter().filter(|&&e| e <= tol).count();
422 compliant as f64 / self.planarity_errors.len().max(1) as f64
423 }
424
425 pub fn gaussian_curvature_at(&self, i: usize, j: usize) -> f64 {
430 if i == 0 || i >= self.nu - 1 || j == 0 || j >= self.nv - 1 {
431 return 0.0; }
433 let idx = |ii: usize, jj: usize| ii * self.nv + jj;
434 let p = self.vertices[idx(i, j)];
435 let pn = self.vertices[idx(i - 1, j)];
437 let ps = self.vertices[idx(i + 1, j)];
438 let pe = self.vertices[idx(i, j + 1)];
439 let pw = self.vertices[idx(i, j - 1)];
440 let angle = |a: V3, centre: V3, b: V3| -> f64 {
442 let u = a.sub(¢re).normalize();
443 let v = b.sub(¢re).normalize();
444 u.dot(&v).clamp(-1.0, 1.0).acos()
445 };
446 let theta_sum = angle(pn, p, pe) + angle(pe, p, ps) + angle(ps, p, pw) + angle(pw, p, pn);
447 let area_approx = {
448 let d1 = pn.sub(&p).norm();
449 let d2 = pe.sub(&p).norm();
450 d1 * d2
451 };
452 if area_approx < 1e-20 {
453 return 0.0;
454 }
455 (2.0 * PI - theta_sum) / area_approx
456 }
457
458 pub fn face_count(&self) -> usize {
460 self.faces.len()
461 }
462}
463
464#[derive(Debug, Clone)]
474pub struct TensileStructure {
475 pub nodes: Vec<V3>,
477 pub cables: Vec<(usize, usize)>,
479 pub prestress: Vec<f64>,
481 pub anchors: Vec<usize>,
483 pub trib_areas: Vec<f64>,
485}
486
487impl TensileStructure {
488 pub fn flat_net(nx: usize, ny: usize, width: f64, height: f64, p0: f64) -> Self {
493 assert!(nx >= 2 && ny >= 2);
494 let mut nodes = Vec::with_capacity(nx * ny);
495 for i in 0..nx {
496 let x = i as f64 / (nx - 1) as f64 * width;
497 for j in 0..ny {
498 let y = j as f64 / (ny - 1) as f64 * height;
499 nodes.push(V3::new(x, y, 0.0));
500 }
501 }
502 let mut cables = Vec::new();
503 for i in 0..nx {
504 for j in 0..ny {
505 let idx = i * ny + j;
506 if j + 1 < ny {
507 cables.push((idx, idx + 1));
508 }
509 if i + 1 < nx {
510 cables.push((idx, idx + ny));
511 }
512 }
513 }
514 let prestress = vec![p0; cables.len()];
515 let mut anchors = Vec::new();
516 for i in 0..nx {
517 for j in 0..ny {
518 if i == 0 || i == nx - 1 || j == 0 || j == ny - 1 {
519 anchors.push(i * ny + j);
520 }
521 }
522 }
523 let trib_areas = vec![(width / (nx - 1) as f64) * (height / (ny - 1) as f64); nodes.len()];
524 Self {
525 nodes,
526 cables,
527 prestress,
528 anchors,
529 trib_areas,
530 }
531 }
532
533 pub fn form_find(
537 &mut self,
538 pressure: f64,
539 max_iter: usize,
540 dt: f64,
541 damping: f64,
542 tol: f64,
543 ) -> f64 {
544 let n = self.nodes.len();
545 let anchors: std::collections::HashSet<usize> = self.anchors.iter().copied().collect();
546 let mut vel = vec![V3::zero(); n];
547 let mass = 1.0_f64; for _iter in 0..max_iter {
550 let mut forces = vec![V3::zero(); n];
551 for (m_idx, (a, b)) in self.cables.iter().enumerate() {
553 let pa = self.nodes[*a];
554 let pb = self.nodes[*b];
555 let delta = pb.sub(&pa);
556 let length = delta.norm();
557 if length < 1e-15 {
558 continue;
559 }
560 let dir = delta.normalize();
561 let f_density = self.prestress[m_idx] / length;
563 let fv = dir.scale(f_density);
564 forces[*a] = forces[*a].add(&fv);
565 forces[*b] = forces[*b].sub(&fv);
566 }
567 for (i, force) in forces.iter_mut().enumerate() {
569 if !anchors.contains(&i) {
570 let fz = pressure * self.trib_areas[i];
571 force.z -= fz;
572 }
573 }
574 let mut max_res = 0.0_f64;
576 for i in 0..n {
577 if anchors.contains(&i) {
578 vel[i] = V3::zero();
579 continue;
580 }
581 let f_norm = forces[i].norm();
582 if f_norm > max_res {
583 max_res = f_norm;
584 }
585 let acc = forces[i].scale(1.0 / mass);
586 vel[i] = vel[i].scale(1.0 - damping).add(&acc.scale(dt));
587 self.nodes[i] = self.nodes[i].add(&vel[i].scale(dt));
588 }
589 if max_res < tol {
590 break;
591 }
592 }
593 self.nodes
595 .iter()
596 .enumerate()
597 .filter(|(i, _)| !anchors.contains(i))
598 .map(|(_, p)| p.z.abs())
599 .fold(0.0_f64, f64::max)
600 }
601
602 pub fn prestress_energy(&self) -> f64 {
607 self.cables
608 .iter()
609 .enumerate()
610 .map(|(m, (a, b))| {
611 let l = self.nodes[*a].sub(&self.nodes[*b]).norm();
612 let t = self.prestress[m];
613 let dl = l - t / 1e3; t / l * dl * dl * 0.5
616 })
617 .sum()
618 }
619
620 pub fn mean_cable_tension(&self) -> f64 {
622 if self.prestress.is_empty() {
623 return 0.0;
624 }
625 self.prestress.iter().sum::<f64>() / self.prestress.len() as f64
626 }
627}
628
629#[derive(Debug, Clone)]
639pub struct GeodesicDome {
640 pub frequency: usize,
642 pub radius: f64,
644 pub vertices: Vec<V3>,
646 pub struts: Vec<(usize, usize)>,
648 pub hemisphere: bool,
650}
651
652impl GeodesicDome {
653 fn icosahedron_vertices(radius: f64) -> Vec<V3> {
655 let phi = (1.0 + 5.0_f64.sqrt()) / 2.0; let s = radius / (1.0 + phi * phi).sqrt();
657 let t = phi * s;
658 vec![
659 V3::new(0.0, s, t),
660 V3::new(0.0, -s, t),
661 V3::new(0.0, s, -t),
662 V3::new(0.0, -s, -t),
663 V3::new(s, t, 0.0),
664 V3::new(-s, t, 0.0),
665 V3::new(s, -t, 0.0),
666 V3::new(-s, -t, 0.0),
667 V3::new(t, 0.0, s),
668 V3::new(-t, 0.0, s),
669 V3::new(t, 0.0, -s),
670 V3::new(-t, 0.0, -s),
671 ]
672 }
673
674 fn icosahedron_faces() -> Vec<[usize; 3]> {
676 vec![
677 [0, 1, 8],
678 [0, 8, 4],
679 [0, 4, 5],
680 [0, 5, 9],
681 [0, 9, 1],
682 [1, 6, 8],
683 [8, 6, 10],
684 [8, 10, 4],
685 [4, 10, 2],
686 [4, 2, 5],
687 [5, 2, 11],
688 [5, 11, 9],
689 [9, 11, 7],
690 [9, 7, 1],
691 [1, 7, 6],
692 [3, 10, 6],
693 [3, 6, 7],
694 [3, 7, 11],
695 [3, 11, 2],
696 [3, 2, 10],
697 ]
698 }
699
700 pub fn new(frequency: usize, radius: f64, hemisphere: bool) -> Self {
704 assert!((1..=8).contains(&frequency), "frequency 1–8 supported");
705 let base_verts = Self::icosahedron_vertices(radius);
706 let base_faces = Self::icosahedron_faces();
707
708 if frequency == 1 {
709 let vertices: Vec<V3> = base_verts
711 .iter()
712 .map(|v| v.normalize().scale(radius))
713 .collect();
714 let mut struts = Vec::new();
715 for face in &base_faces {
716 for k in 0..3 {
717 let a = face[k];
718 let b = face[(k + 1) % 3];
719 let edge = if a < b { (a, b) } else { (b, a) };
720 if !struts.contains(&edge) {
721 struts.push(edge);
722 }
723 }
724 }
725 return Self {
726 frequency,
727 radius,
728 vertices,
729 struts,
730 hemisphere,
731 };
732 }
733
734 let mut all_points: Vec<V3> = Vec::new();
736 let mut all_struts: Vec<(usize, usize)> = Vec::new();
737
738 let find_or_insert = |pts: &mut Vec<V3>, p: V3| -> usize {
739 let threshold = radius * 1e-6;
740 for (idx, existing) in pts.iter().enumerate() {
741 if existing.sub(&p).norm() < threshold {
742 return idx;
743 }
744 }
745 pts.push(p);
746 pts.len() - 1
747 };
748
749 for face in &base_faces {
750 let va = base_verts[face[0]].normalize().scale(radius);
751 let vb = base_verts[face[1]].normalize().scale(radius);
752 let vc = base_verts[face[2]].normalize().scale(radius);
753
754 let f = frequency as f64;
755 let mut local: Vec<Vec<V3>> = vec![vec![V3::zero(); frequency + 1]; frequency + 1];
757 for (i, local_row) in local.iter_mut().enumerate() {
758 for (j, local_ij) in local_row.iter_mut().enumerate().take(frequency + 1 - i) {
759 let k = frequency - i - j;
760 let p = V3::new(
761 (i as f64 * va.x + j as f64 * vb.x + k as f64 * vc.x) / f,
762 (i as f64 * va.y + j as f64 * vb.y + k as f64 * vc.y) / f,
763 (i as f64 * va.z + j as f64 * vb.z + k as f64 * vc.z) / f,
764 );
765 *local_ij = p.normalize().scale(radius);
767 }
768 }
769
770 for i in 0..=frequency {
772 for j in 0..=frequency - i {
773 let idx_a = find_or_insert(&mut all_points, local[i][j]);
774 if j < frequency - i {
775 let idx_b = find_or_insert(&mut all_points, local[i][j + 1]);
776 let e = if idx_a < idx_b {
777 (idx_a, idx_b)
778 } else {
779 (idx_b, idx_a)
780 };
781 if !all_struts.contains(&e) {
782 all_struts.push(e);
783 }
784 }
785 if i < frequency - j {
786 let idx_c = find_or_insert(&mut all_points, local[i + 1][j]);
787 let e = if idx_a < idx_c {
788 (idx_a, idx_c)
789 } else {
790 (idx_c, idx_a)
791 };
792 if !all_struts.contains(&e) {
793 all_struts.push(e);
794 }
795 }
796 if i < frequency - j && j < frequency - (i + 1) {
797 let idx_b = find_or_insert(&mut all_points, local[i][j + 1]);
798 let idx_c = find_or_insert(&mut all_points, local[i + 1][j]);
799 let e = if idx_b < idx_c {
800 (idx_b, idx_c)
801 } else {
802 (idx_c, idx_b)
803 };
804 if !all_struts.contains(&e) {
805 all_struts.push(e);
806 }
807 }
808 }
809 }
810 }
811
812 let vertices = all_points;
813 let struts = all_struts;
814
815 Self {
816 frequency,
817 radius,
818 vertices,
819 struts,
820 hemisphere,
821 }
822 }
823
824 pub fn expected_vertex_count(frequency: usize) -> usize {
828 10 * frequency * frequency + 2
829 }
830
831 pub fn expected_strut_count(frequency: usize) -> usize {
835 30 * frequency * frequency
836 }
837
838 pub fn strut_length_stats(&self) -> (f64, f64, f64) {
840 if self.struts.is_empty() {
841 return (0.0, 0.0, 0.0);
842 }
843 let lengths: Vec<f64> = self
844 .struts
845 .iter()
846 .map(|(a, b)| self.vertices[*a].sub(&self.vertices[*b]).norm())
847 .collect();
848 let min = lengths.iter().cloned().fold(f64::MAX, f64::min);
849 let max = lengths.iter().cloned().fold(0.0_f64, f64::max);
850 let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
851 (min, max, mean)
852 }
853
854 pub fn verify_sphericity(&self, tol: f64) -> bool {
856 self.vertices
857 .iter()
858 .all(|v| (v.norm() - self.radius).abs() < tol)
859 }
860}
861
862#[derive(Debug, Clone)]
870pub struct StructuralGlass {
871 pub width: f64,
873 pub height: f64,
875 pub thickness: f64,
877 pub e_glass: f64,
879 pub nu_glass: f64,
881 pub density: f64,
883 pub wind_pressure: f64,
885 pub delta_temp: f64,
887 pub alpha_cte: f64,
889 pub sealant_width: f64,
891 pub sealant_shear_strength: f64,
893}
894
895impl StructuralGlass {
896 pub fn monolithic_10mm() -> Self {
898 Self {
899 width: 3.0,
900 height: 5.0,
901 thickness: 0.01,
902 e_glass: 70.0e9,
903 nu_glass: 0.23,
904 density: 2500.0,
905 wind_pressure: 1200.0, delta_temp: 40.0,
907 alpha_cte: 9.0e-6,
908 sealant_width: 0.025,
909 sealant_shear_strength: 0.14e6,
910 }
911 }
912
913 pub fn self_weight_pressure(&self) -> f64 {
915 self.density * 9.81 * self.thickness
916 }
917
918 pub fn max_bending_stress_wind(&self) -> f64 {
924 let a = self.width.min(self.height);
925 let alpha_coeff = 0.287_f64; alpha_coeff * self.wind_pressure * a * a / (self.thickness * self.thickness)
927 }
928
929 pub fn thermal_stress(&self) -> f64 {
933 self.e_glass * self.alpha_cte * self.delta_temp
934 }
935
936 pub fn combined_stress(&self) -> f64 {
938 self.max_bending_stress_wind() + self.thermal_stress()
939 }
940
941 pub fn sealant_shear_capacity(&self) -> f64 {
946 self.sealant_shear_strength * self.sealant_width
947 }
948
949 pub fn edge_reaction_wind(&self) -> f64 {
952 let area = self.width * self.height;
953 let perimeter = 2.0 * (self.width + self.height);
955 self.wind_pressure * area / perimeter
956 }
957
958 pub fn centre_deflection_wind(&self) -> f64 {
963 let a = self.width.min(self.height);
964 let beta = 0.0443_f64;
965 beta * self.wind_pressure * a.powi(4) / (self.e_glass * self.thickness.powi(3))
966 }
967
968 pub fn span_deflection_ratio(&self) -> f64 {
970 let a = self.width.min(self.height);
971 let delta = self.centre_deflection_wind();
972 if delta < 1e-12 {
973 return f64::MAX;
974 }
975 a / delta
976 }
977
978 pub fn igu_self_weight_pressure(&self) -> f64 {
982 let cavity_mass_per_m2 = 1.784 * 0.016; (2.0 * self.density * self.thickness + cavity_mass_per_m2) * 9.81
984 }
985}
986
987#[derive(Debug, Clone)]
996pub struct ParametricFacade {
997 pub bays: usize,
999 pub levels: usize,
1001 pub bay_width: f64,
1003 pub storey_height: f64,
1005 pub facade_azimuth: f64,
1007 pub latitude: f64,
1009 pub max_shade_depth: f64,
1011 pub target_solar_exposure: f64,
1013}
1014
1015impl ParametricFacade {
1016 pub fn tokyo_south() -> Self {
1018 Self {
1019 bays: 8,
1020 levels: 10,
1021 bay_width: 1.5,
1022 storey_height: 3.6,
1023 facade_azimuth: 180.0,
1024 latitude: 35.7,
1025 max_shade_depth: 0.8,
1026 target_solar_exposure: 400.0,
1027 }
1028 }
1029
1030 pub fn solar_altitude(&self, day_of_year: f64, hour: f64) -> f64 {
1034 let lat_rad = self.latitude.to_radians();
1035 let decl =
1037 23.45_f64.to_radians() * (360.0 / 365.0 * (day_of_year - 81.0)).to_radians().sin();
1038 let hour_angle = (hour - 12.0) * 15.0; let h_rad = hour_angle.to_radians();
1040 let sin_alt = lat_rad.sin() * decl.sin() + lat_rad.cos() * decl.cos() * h_rad.cos();
1041 sin_alt.clamp(-1.0, 1.0).asin().to_degrees()
1042 }
1043
1044 pub fn solar_azimuth(&self, day_of_year: f64, hour: f64) -> f64 {
1047 let lat_rad = self.latitude.to_radians();
1048 let decl =
1049 23.45_f64.to_radians() * (360.0 / 365.0 * (day_of_year - 81.0)).to_radians().sin();
1050 let hour_angle = (hour - 12.0) * 15.0;
1051 let h_rad = hour_angle.to_radians();
1052 let sin_alt = lat_rad.sin() * decl.sin() + lat_rad.cos() * decl.cos() * h_rad.cos();
1053 let cos_az = (decl.sin() - lat_rad.sin() * sin_alt)
1054 / (lat_rad.cos() * sin_alt.asin().cos().max(1e-10));
1055 let az_deg = cos_az.clamp(-1.0, 1.0).acos().to_degrees();
1056 if hour_angle > 0.0 { az_deg } else { -az_deg }
1057 }
1058
1059 pub fn incidence_angle(&self, alt_deg: f64, az_deg: f64) -> f64 {
1062 let facade_az = self.facade_azimuth - 180.0; let delta_az = (az_deg - facade_az).to_radians();
1064 let alt_rad = alt_deg.to_radians();
1065 let cos_inc = alt_rad.cos() * delta_az.cos();
1067 cos_inc.clamp(-1.0, 1.0).acos().to_degrees()
1068 }
1069
1070 pub fn required_shade_depth(&self, incidence_deg: f64) -> f64 {
1074 let inc_from_horiz = (90.0 - incidence_deg).abs().to_radians();
1075 let depth = self.storey_height * inc_from_horiz.tan();
1076 depth.min(self.max_shade_depth)
1077 }
1078
1079 pub fn shade_depth_matrix(&self) -> Vec<Vec<f64>> {
1083 let alt = self.solar_altitude(172.0, 12.0); let az = self.solar_azimuth(172.0, 12.0);
1085 let inc = self.incidence_angle(alt, az);
1086 let depth = self.required_shade_depth(inc);
1087 vec![vec![depth; self.bays]; self.levels]
1088 }
1089
1090 pub fn annual_solar_exposure(&self) -> f64 {
1095 let panel_area = self.bay_width * self.storey_height;
1096 let mut total_irradiance = 0.0_f64;
1097 let dni = 800.0_f64; let months = [
1099 17.0, 47.0, 75.0, 105.0, 135.0, 162.0, 198.0, 228.0, 258.0, 288.0, 318.0, 344.0_f64,
1100 ];
1101 for &day in &months {
1102 for h_int in 7..17 {
1103 let hour = h_int as f64 + 0.5;
1104 let alt = self.solar_altitude(day, hour);
1105 if alt <= 0.0 {
1106 continue;
1107 }
1108 let az = self.solar_azimuth(day, hour);
1109 let inc = self.incidence_angle(alt, az);
1110 let cos_inc = inc.to_radians().cos().max(0.0);
1111 total_irradiance += dni * cos_inc;
1112 }
1113 }
1114 total_irradiance / 1000.0 * panel_area
1116 }
1117
1118 pub fn total_panel_count(&self) -> usize {
1120 self.bays * self.levels
1121 }
1122
1123 pub fn total_facade_area(&self) -> f64 {
1125 self.bays as f64 * self.bay_width * self.levels as f64 * self.storey_height
1126 }
1127
1128 pub fn window_to_wall_ratio(&self) -> f64 {
1132 0.70
1133 }
1134
1135 pub fn shading_performance_index(&self) -> f64 {
1139 let matrix = self.shade_depth_matrix();
1140 let mean_depth: f64 =
1141 matrix.iter().flatten().sum::<f64>() / (self.bays * self.levels) as f64;
1142 (mean_depth / self.max_shade_depth).min(1.0)
1143 }
1144}
1145
1146pub fn triangle_area(a: V3, b: V3, c: V3) -> f64 {
1152 let ab = b.sub(&a);
1153 let ac = c.sub(&a);
1154 ab.cross(&ac).norm() * 0.5
1155}
1156
1157pub fn fit_plane(points: &[V3]) -> (V3, V3) {
1162 if points.len() < 3 {
1163 return (V3::zero(), V3::new(0.0, 0.0, 1.0));
1164 }
1165 let n = points.len() as f64;
1166 let cx = points.iter().map(|p| p.x).sum::<f64>() / n;
1167 let cy = points.iter().map(|p| p.y).sum::<f64>() / n;
1168 let cz = points.iter().map(|p| p.z).sum::<f64>() / n;
1169 let centroid = V3::new(cx, cy, cz);
1170
1171 let (mut cxx, mut cxy, mut cxz, mut cyy, mut cyz, mut _czz) =
1173 (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
1174 for p in points {
1175 let dx = p.x - cx;
1176 let dy = p.y - cy;
1177 let dz = p.z - cz;
1178 cxx += dx * dx;
1179 cxy += dx * dy;
1180 cxz += dx * dz;
1181 cyy += dy * dy;
1182 cyz += dy * dz;
1183 _czz += dz * dz;
1184 }
1185 let v1 = V3::new(cxx, cxy, cxz).normalize();
1189 let v2 = V3::new(cxy, cyy, cyz).normalize();
1190 let normal = v1.cross(&v2).normalize();
1191 (centroid, normal)
1192}
1193
1194pub fn point_to_plane_distance(p: V3, origin: V3, normal: V3) -> f64 {
1196 p.sub(&origin).dot(&normal).abs()
1197}
1198
1199pub fn perimeter_resample(pts: &[V3], n: usize) -> Vec<V3> {
1203 if pts.len() < 2 || n == 0 {
1204 return Vec::new();
1205 }
1206 let mut arcs = vec![0.0_f64; pts.len()];
1208 for i in 1..pts.len() {
1209 arcs[i] = arcs[i - 1] + pts[i].sub(&pts[i - 1]).norm();
1210 }
1211 let total = *arcs.last().expect("collection should not be empty");
1212 let mut result = Vec::with_capacity(n);
1213 for k in 0..n {
1214 let s = k as f64 / n as f64 * total;
1215 let seg = arcs.partition_point(|&a| a <= s).min(pts.len() - 1);
1217 let seg = seg.max(1);
1218 let t0 = arcs[seg - 1];
1219 let t1 = arcs[seg];
1220 let t = if (t1 - t0).abs() < 1e-15 {
1221 0.0
1222 } else {
1223 (s - t0) / (t1 - t0)
1224 };
1225 let p = pts[seg - 1].scale(1.0 - t).add(&pts[seg].scale(t));
1226 result.push(p);
1227 }
1228 result
1229}
1230
1231#[cfg(test)]
1236mod tests {
1237 use super::*;
1238
1239 #[test]
1242 fn test_v3_norm() {
1243 let v = V3::new(3.0, 4.0, 0.0);
1244 assert!((v.norm() - 5.0).abs() < 1e-10);
1245 }
1246
1247 #[test]
1248 fn test_v3_normalize() {
1249 let v = V3::new(1.0, 2.0, 2.0);
1250 let n = v.normalize();
1251 assert!((n.norm() - 1.0).abs() < 1e-10);
1252 }
1253
1254 #[test]
1255 fn test_v3_cross_orthogonal() {
1256 let a = V3::new(1.0, 0.0, 0.0);
1257 let b = V3::new(0.0, 1.0, 0.0);
1258 let c = a.cross(&b);
1259 assert!((c.z - 1.0).abs() < 1e-10);
1260 assert!(c.x.abs() < 1e-10 && c.y.abs() < 1e-10);
1261 }
1262
1263 #[test]
1264 fn test_v3_dot() {
1265 let a = V3::new(1.0, 2.0, 3.0);
1266 let b = V3::new(4.0, 5.0, 6.0);
1267 assert!((a.dot(&b) - 32.0).abs() < 1e-10);
1268 }
1269
1270 #[test]
1273 fn test_grid_shell_node_count() {
1274 let gs = GridShell::parabolic(5, 6, 20.0, 15.0, 3.0);
1275 assert_eq!(gs.node_count(), 30);
1276 }
1277
1278 #[test]
1279 fn test_grid_shell_dynamic_relaxation_converges() {
1280 let mut gs = GridShell::parabolic(5, 5, 10.0, 10.0, 2.0);
1281 let residual = gs.dynamic_relaxation(500, 0.001, 0.1, 1.0);
1282 assert!(residual < 1.0e6, "residual too high: {residual}");
1284 }
1285
1286 #[test]
1287 fn test_grid_shell_fixed_nodes_unchanged() {
1288 let mut gs = GridShell::parabolic(4, 4, 8.0, 8.0, 1.5);
1289 let initial_positions: Vec<V3> = gs.fixed_nodes.iter().map(|&i| gs.nodes[i]).collect();
1290 gs.dynamic_relaxation(100, 0.001, 0.1, 1.0);
1291 for (k, &idx) in gs.fixed_nodes.iter().enumerate() {
1292 let diff = gs.nodes[idx].sub(&initial_positions[k]).norm();
1293 assert!(diff < 1e-10, "fixed node {idx} moved by {diff}");
1294 }
1295 }
1296
1297 #[test]
1298 fn test_grid_shell_member_count() {
1299 let gs = GridShell::parabolic(3, 3, 6.0, 6.0, 1.0);
1300 assert_eq!(gs.member_count(), 20);
1302 }
1303
1304 #[test]
1307 fn test_planar_quad_planarity_zero() {
1308 let pts = [
1310 V3::new(0.0, 0.0, 0.0),
1311 V3::new(1.0, 0.0, 0.0),
1312 V3::new(1.0, 1.0, 0.0),
1313 V3::new(0.0, 1.0, 0.0),
1314 ];
1315 let err = PanelizationResult::quad_planarity_error(&pts);
1316 assert!(err < 1e-12, "planarity error = {err}");
1317 }
1318
1319 #[test]
1320 fn test_planar_quad_planarity_nonzero() {
1321 let pts = [
1322 V3::new(0.0, 0.0, 0.0),
1323 V3::new(1.0, 0.0, 0.0),
1324 V3::new(1.0, 1.0, 0.0),
1325 V3::new(0.0, 1.0, 0.1), ];
1327 let err = PanelizationResult::quad_planarity_error(&pts);
1328 assert!(err > 0.05, "expected non-zero planarity error, got {err}");
1329 }
1330
1331 #[test]
1332 fn test_planar_quad_mesh_flat_surface_zero_error() {
1333 let mesh = PlanarQuadMesh::from_surface(5, 5, |u, v| V3::new(u, v, 0.0));
1334 assert!(mesh.max_planarity_error() < 1e-12);
1335 }
1336
1337 #[test]
1338 fn test_planar_quad_mesh_curved_surface_error_positive() {
1339 let mesh = PlanarQuadMesh::from_surface(10, 10, |u, v| {
1340 V3::new(u, v, (u * PI * 2.0).sin() * (v * PI * 2.0).cos() * 0.3)
1341 });
1342 assert!(mesh.max_planarity_error() >= 0.0);
1344 }
1345
1346 #[test]
1347 fn test_planar_quad_mesh_face_count() {
1348 let mesh = PlanarQuadMesh::from_surface(6, 8, |u, v| V3::new(u, v, 0.0));
1349 assert_eq!(mesh.face_count(), 5 * 7);
1350 }
1351
1352 #[test]
1353 fn test_planar_quad_mesh_compliance_ratio_flat() {
1354 let mesh = PlanarQuadMesh::from_surface(5, 5, |u, v| V3::new(u, v, 0.0));
1355 assert!((mesh.planarity_compliance_ratio(0.001) - 1.0).abs() < 1e-10);
1356 }
1357
1358 #[test]
1361 fn test_tensile_flat_net_node_count() {
1362 let net = TensileStructure::flat_net(5, 5, 10.0, 10.0, 5000.0);
1363 assert_eq!(net.nodes.len(), 25);
1364 }
1365
1366 #[test]
1367 fn test_tensile_form_find_sag_positive() {
1368 let mut net = TensileStructure::flat_net(5, 5, 10.0, 10.0, 10000.0);
1369 let sag = net.form_find(500.0, 1000, 0.001, 0.1, 0.01);
1370 assert!(sag >= 0.0, "sag should be non-negative, got {sag}");
1371 }
1372
1373 #[test]
1374 fn test_tensile_mean_cable_tension() {
1375 let net = TensileStructure::flat_net(4, 4, 8.0, 8.0, 3000.0);
1376 let mean = net.mean_cable_tension();
1377 assert!((mean - 3000.0).abs() < 1e-6);
1378 }
1379
1380 #[test]
1381 fn test_tensile_anchor_nodes_not_moved() {
1382 let mut net = TensileStructure::flat_net(4, 4, 6.0, 6.0, 5000.0);
1383 let initial: Vec<V3> = net.anchors.iter().map(|&i| net.nodes[i]).collect();
1384 net.form_find(200.0, 200, 0.001, 0.2, 0.1);
1385 for (k, &idx) in net.anchors.iter().enumerate() {
1386 let diff = net.nodes[idx].sub(&initial[k]).norm();
1387 assert!(diff < 1e-10, "anchor {idx} moved by {diff}");
1388 }
1389 }
1390
1391 #[test]
1394 fn test_geodesic_dome_freq1_vertex_count() {
1395 let dome = GeodesicDome::new(1, 5.0, false);
1396 assert_eq!(dome.vertices.len(), 12);
1398 }
1399
1400 #[test]
1401 fn test_geodesic_dome_freq2_expected_vertices() {
1402 let dome = GeodesicDome::new(2, 5.0, false);
1403 let expected = GeodesicDome::expected_vertex_count(2); let actual = dome.vertices.len();
1406 assert!(
1407 (actual as i64 - expected as i64).abs() <= 5,
1408 "expected ~{expected} vertices, got {actual}"
1409 );
1410 }
1411
1412 #[test]
1413 fn test_geodesic_dome_all_on_sphere() {
1414 let dome = GeodesicDome::new(2, 7.0, false);
1415 assert!(
1416 dome.verify_sphericity(1e-6),
1417 "vertices not on sphere within tolerance"
1418 );
1419 }
1420
1421 #[test]
1422 fn test_geodesic_dome_strut_lengths_similar() {
1423 let dome = GeodesicDome::new(2, 5.0, false);
1424 let (min, max, _mean) = dome.strut_length_stats();
1425 assert!(
1427 max / min.max(1e-10) < 1.20,
1428 "strut length ratio {:.3} too large",
1429 max / min
1430 );
1431 }
1432
1433 #[test]
1434 fn test_geodesic_dome_freq1_strut_count() {
1435 let dome = GeodesicDome::new(1, 5.0, false);
1436 assert_eq!(dome.struts.len(), 30);
1438 }
1439
1440 #[test]
1443 fn test_glass_self_weight_positive() {
1444 let g = StructuralGlass::monolithic_10mm();
1445 let sw = g.self_weight_pressure();
1446 assert!(sw > 0.0 && sw < 500.0, "self weight = {sw} Pa");
1447 }
1448
1449 #[test]
1450 fn test_glass_thermal_stress() {
1451 let g = StructuralGlass::monolithic_10mm();
1452 let ts = g.thermal_stress();
1453 assert!((ts - 25.2e6).abs() < 1.0e5);
1455 }
1456
1457 #[test]
1458 fn test_glass_wind_stress_positive() {
1459 let g = StructuralGlass::monolithic_10mm();
1460 let ws = g.max_bending_stress_wind();
1461 assert!(ws > 0.0);
1462 }
1463
1464 #[test]
1465 fn test_glass_combined_stress_greater_than_parts() {
1466 let g = StructuralGlass::monolithic_10mm();
1467 let combined = g.combined_stress();
1468 assert!(combined > g.thermal_stress());
1469 assert!(combined > g.max_bending_stress_wind());
1470 }
1471
1472 #[test]
1473 fn test_glass_centre_deflection() {
1474 let g = StructuralGlass::monolithic_10mm();
1475 let d = g.centre_deflection_wind();
1476 assert!(d > 0.0 && d < 0.2, "deflection = {d} m");
1477 }
1478
1479 #[test]
1480 fn test_glass_sealant_capacity_positive() {
1481 let g = StructuralGlass::monolithic_10mm();
1482 assert!(g.sealant_shear_capacity() > 0.0);
1483 }
1484
1485 #[test]
1488 fn test_facade_total_panel_count() {
1489 let f = ParametricFacade::tokyo_south();
1490 assert_eq!(f.total_panel_count(), 80); }
1492
1493 #[test]
1494 fn test_facade_solar_altitude_summer_positive() {
1495 let f = ParametricFacade::tokyo_south();
1496 let alt = f.solar_altitude(172.0, 12.0);
1497 assert!(alt > 0.0, "summer noon altitude should be positive: {alt}");
1498 }
1499
1500 #[test]
1501 fn test_facade_solar_altitude_night_negative() {
1502 let f = ParametricFacade::tokyo_south();
1503 let alt = f.solar_altitude(172.0, 0.0); assert!(alt < 0.0, "midnight altitude should be negative: {alt}");
1505 }
1506
1507 #[test]
1508 fn test_facade_shade_depth_matrix_dimensions() {
1509 let f = ParametricFacade::tokyo_south();
1510 let m = f.shade_depth_matrix();
1511 assert_eq!(m.len(), f.levels);
1512 assert_eq!(m[0].len(), f.bays);
1513 }
1514
1515 #[test]
1516 fn test_facade_shade_depth_bounded() {
1517 let f = ParametricFacade::tokyo_south();
1518 let m = f.shade_depth_matrix();
1519 for row in &m {
1520 for &d in row {
1521 assert!(d >= 0.0 && d <= f.max_shade_depth + 1e-10, "depth = {d}");
1522 }
1523 }
1524 }
1525
1526 #[test]
1527 fn test_facade_total_area() {
1528 let f = ParametricFacade::tokyo_south();
1529 let area = f.total_facade_area();
1530 assert!((area - 432.0).abs() < 1e-6);
1532 }
1533
1534 #[test]
1535 fn test_facade_annual_solar_exposure_positive() {
1536 let f = ParametricFacade::tokyo_south();
1537 let exp = f.annual_solar_exposure();
1538 assert!(
1539 exp >= 0.0,
1540 "annual solar exposure should be non-negative: {exp}"
1541 );
1542 }
1543
1544 #[test]
1547 fn test_triangle_area() {
1548 let a = V3::new(0.0, 0.0, 0.0);
1549 let b = V3::new(4.0, 0.0, 0.0);
1550 let c = V3::new(0.0, 3.0, 0.0);
1551 let area = triangle_area(a, b, c);
1552 assert!((area - 6.0).abs() < 1e-10);
1553 }
1554
1555 #[test]
1556 fn test_fit_plane_xy_plane() {
1557 let pts = vec![
1558 V3::new(0.0, 0.0, 0.0),
1559 V3::new(1.0, 0.0, 0.0),
1560 V3::new(0.0, 1.0, 0.0),
1561 V3::new(1.0, 1.0, 0.0),
1562 ];
1563 let (_centroid, normal) = fit_plane(&pts);
1564 assert!(normal.z.abs() > 0.5, "normal.z = {}", normal.z);
1566 }
1567
1568 #[test]
1569 fn test_perimeter_resample_count() {
1570 let pts = vec![
1571 V3::new(0.0, 0.0, 0.0),
1572 V3::new(1.0, 0.0, 0.0),
1573 V3::new(1.0, 1.0, 0.0),
1574 V3::new(0.0, 1.0, 0.0),
1575 ];
1576 let resampled = perimeter_resample(&pts, 8);
1577 assert_eq!(resampled.len(), 8);
1578 }
1579
1580 #[test]
1581 fn test_point_to_plane_distance() {
1582 let p = V3::new(0.0, 0.0, 5.0);
1583 let origin = V3::zero();
1584 let normal = V3::new(0.0, 0.0, 1.0);
1585 let d = point_to_plane_distance(p, origin, normal);
1586 assert!((d - 5.0).abs() < 1e-10);
1587 }
1588}