1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
//! Thin-Wall Vibration & Printer Resonance Risk
//!
//! Phase E2 of the ALICE-Physics completeness project. FDM printed parts
//! with thin planar walls (< 3 mm) can resonate with printer-generated
//! vibration and produce "spaghetti"-like buckling, layer smearing, or
//! post-print acoustic buzz. This module compares a wall's first natural
//! frequency (from `modal::plate_natural_frequency_hz`) against a database
//! of common excitation sources and flags dangerous overlaps.
//!
//! # Typical excitation sources (Hz)
//!
//! | Source | Frequency |
//! |--------|-----------|
//! | Bambu X1C X/Y stepper (movement) | 60 - 200 Hz |
//! | Z lead-screw stepper | 20 - 80 Hz |
//! | Cooling fan (5000-8000 rpm) | 80 - 130 Hz |
//! | Chamber HVAC | 50 - 60 Hz |
//! | Extruder gear whine | 300 - 800 Hz |
//! | Handling / transport shock | 5 - 20 Hz |
//!
//! # Resonance criterion
//!
//! A wall is at risk when its natural frequency is within ±20 % of a
//! significant excitation source. Beyond that, the wall's damping (typically
//! 5-10 % for FDM plastics) reduces resonance amplification below 3×.
//!
//! # References
//!
//! - Meirovitch, *Fundamentals of Vibrations* Ch. 4.
//! - Bambu Lab X1C service manual, motor / fan specifications.
//! - Blevins, *Formulas for Natural Frequency and Mode Shape*.
use crate::filament_db::MaterialProperties;
use crate::math::Fix128;
use crate::modal::plate_natural_frequency_hz;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
// ============================================================================
// Excitation sources
// ============================================================================
/// A named vibration excitation source.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExcitationSource {
/// Human-readable source name.
pub name: &'static str,
/// Nominal excitation frequency (Hz).
pub frequency_hz: Fix128,
}
/// Curated defaults for common printer / environmental sources.
#[must_use]
pub fn default_excitation_sources() -> Vec<ExcitationSource> {
vec![
ExcitationSource {
name: "X/Y stepper (Bambu X1C)",
frequency_hz: Fix128::from_int(120),
},
ExcitationSource {
name: "Z lead-screw stepper",
frequency_hz: Fix128::from_int(40),
},
ExcitationSource {
name: "Cooling fan 7000rpm",
frequency_hz: Fix128::from_int(117),
},
ExcitationSource {
name: "Chamber HVAC 50Hz",
frequency_hz: Fix128::from_int(50),
},
ExcitationSource {
name: "Extruder gear whine",
frequency_hz: Fix128::from_int(400),
},
ExcitationSource {
name: "Handling / transport shock",
frequency_hz: Fix128::from_int(15),
},
]
}
// ============================================================================
// Resonance report
// ============================================================================
/// Analysis output.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WallResonanceReport {
/// Wall's first natural frequency (Hz).
pub wall_frequency_hz: Fix128,
/// Nearest excitation source (by absolute frequency difference).
pub nearest_source: ExcitationSource,
/// Ratio `wall / source` — should be far from 1.0 for safety.
pub frequency_ratio: Fix128,
/// True iff the wall frequency lies within `resonance_band` of the
/// nearest source (default ±20 %).
pub is_risky: bool,
}
/// Analyse a rectangular wall against a set of excitation sources.
///
/// `wall_thickness_mm`, `side_a_mm`, `side_b_mm` describe a simply-supported
/// rectangular plate (same convention as `modal::plate_natural_frequency_hz`).
/// `resonance_band` = fractional half-width of the risky region (0.2 = ±20 %).
#[must_use]
pub fn analyze_wall_resonance(
material: &MaterialProperties,
poisson_ratio: Fix128,
wall_thickness_mm: Fix128,
side_a_mm: Fix128,
side_b_mm: Fix128,
sources: &[ExcitationSource],
resonance_band: Fix128,
) -> WallResonanceReport {
let f_wall = plate_natural_frequency_hz(
material,
poisson_ratio,
wall_thickness_mm,
side_a_mm,
side_b_mm,
);
// Locate the nearest source by absolute distance.
let mut nearest = sources[0];
let mut best_dist = (f_wall - nearest.frequency_hz).abs();
for src in sources.iter().skip(1) {
let d = (f_wall - src.frequency_hz).abs();
if d < best_dist {
best_dist = d;
nearest = *src;
}
}
let ratio = if nearest.frequency_hz.is_zero() {
Fix128::ZERO
} else {
f_wall / nearest.frequency_hz
};
let is_risky = ratio > (Fix128::ONE - resonance_band) && ratio < (Fix128::ONE + resonance_band);
WallResonanceReport {
wall_frequency_hz: f_wall,
nearest_source: nearest,
frequency_ratio: ratio,
is_risky,
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_sources_include_x1c_stepper() {
let sources = default_excitation_sources();
assert!(sources.iter().any(|s| s.name.contains("X1C")));
assert!(sources.iter().any(|s| s.name.contains("HVAC")));
}
#[test]
fn thick_wall_high_frequency_safe() {
// 5mm thick PLA plate 50×50mm — expected f > 1000 Hz, no source overlap
let sources = default_excitation_sources();
let report = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(5),
Fix128::from_int(50),
Fix128::from_int(50),
&sources,
Fix128::from_ratio(20, 100),
);
assert!(report.wall_frequency_hz > Fix128::from_int(1000));
// Default sources max at 400 Hz → wall_freq/src >> 1 → not in band
// (ratio would need to be near 1)
assert!(!report.is_risky, "5mm/50mm wall should be safe");
}
#[test]
fn thin_large_wall_can_resonate() {
// 1mm thick 200×200 PLA plate — low natural frequency
let sources = default_excitation_sources();
let report = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(1),
Fix128::from_int(200),
Fix128::from_int(200),
&sources,
Fix128::from_ratio(20, 100),
);
// wall_frequency should be < 200 Hz
assert!(report.wall_frequency_hz < Fix128::from_int(500));
// nearest source should be identified from the default list
assert!(report.nearest_source.frequency_hz > Fix128::ZERO);
}
#[test]
fn ratio_computed_from_nearest() {
let sources = default_excitation_sources();
let report = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(100),
Fix128::from_int(100),
&sources,
Fix128::from_ratio(20, 100),
);
assert_ne!(report.frequency_ratio, Fix128::ZERO);
assert_eq!(
report.frequency_ratio,
report.wall_frequency_hz / report.nearest_source.frequency_hz
);
}
#[test]
fn resonance_band_wider_more_risky() {
// Same wall geometry, evaluate with tight vs loose band
let sources = default_excitation_sources();
// Use a moderate geometry where wall_freq is near ~100 Hz range
let thin_report_tight = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(120),
Fix128::from_int(120),
&sources,
Fix128::from_ratio(5, 100),
);
let thin_report_loose = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(120),
Fix128::from_int(120),
&sources,
Fix128::from_ratio(50, 100),
);
// Loose band cannot be less risky than tight band
assert!(thin_report_loose.is_risky || !thin_report_tight.is_risky);
}
#[test]
fn custom_source_can_be_picked() {
let sources = vec![ExcitationSource {
name: "custom",
frequency_hz: Fix128::from_int(500),
}];
let report = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(50),
Fix128::from_int(50),
&sources,
Fix128::from_ratio(20, 100),
);
assert_eq!(report.nearest_source.name, "custom");
}
#[test]
fn abs_wall_vs_pla_different_frequency() {
let sources = default_excitation_sources();
let f_pla = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(100),
Fix128::from_int(100),
&sources,
Fix128::from_ratio(20, 100),
)
.wall_frequency_hz;
let f_abs = analyze_wall_resonance(
&MaterialProperties::abs(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(100),
Fix128::from_int(100),
&sources,
Fix128::from_ratio(20, 100),
)
.wall_frequency_hz;
// Material with different E → different frequency
assert_ne!(f_pla, f_abs);
}
}
// Convenience: analyze uses default sources when caller supplies `&sources`
// as `&default_excitation_sources()`. The signature is generic across custom
// source lists — no extra overload required.
#[cfg(test)]
mod default_convenience {
use super::*;
#[test]
fn helper_call_syntax_smoke() {
let sources = default_excitation_sources();
let _report = analyze_wall_resonance(
&MaterialProperties::pla(),
Fix128::from_ratio(35, 100),
Fix128::from_int(2),
Fix128::from_int(100),
Fix128::from_int(100),
&sources,
Fix128::from_ratio(20, 100),
);
}
}