1use std::collections::HashMap;
2use std::hash::Hash;
3
4use slotmap::new_key_type;
5
6use super::{ElementId, NodeId, NDF};
7
8new_key_type! {
9 pub struct LoadPatternId;
11}
12
13#[derive(Debug, Clone)]
20pub enum LoadSeries {
21 Constant,
24 Linear { slope: f64 },
28 Path { times: Vec<f64>, factors: Vec<f64> },
41 BoundedPath {
44 times: Vec<f64>,
45 factors: Vec<f64>,
46 use_last: bool,
47 },
48}
49
50impl LoadSeries {
51 pub(crate) fn factor(&self, pseudo_time: f64) -> f64 {
52 match self {
53 LoadSeries::Constant => 1.0,
54 LoadSeries::Linear { slope } => slope * pseudo_time,
55 LoadSeries::Path { times, factors } => path_interpolate(times, factors, pseudo_time),
56 LoadSeries::BoundedPath {
57 times,
58 factors,
59 use_last,
60 } => {
61 if pseudo_time < times[0] || (!use_last && pseudo_time > times[times.len() - 1]) {
62 0.0
63 } else {
64 path_interpolate(times, factors, pseudo_time)
65 }
66 }
67 }
68 }
69
70 pub(crate) fn slope(&self, pseudo_time: f64) -> f64 {
79 match self {
80 LoadSeries::Constant => 0.0,
81 LoadSeries::Linear { slope } => *slope,
82 LoadSeries::Path { times, factors }
83 | LoadSeries::BoundedPath { times, factors, .. } => {
84 path_slope(times, factors, pseudo_time)
85 }
86 }
87 }
88}
89
90fn path_segment(times: &[f64], pseudo_time: f64) -> usize {
91 let n = times.partition_point(|&t| t <= pseudo_time);
97 n.saturating_sub(1).min(times.len() - 2)
98}
99
100fn path_interpolate(times: &[f64], factors: &[f64], pseudo_time: f64) -> f64 {
101 if times.len() == 1 {
102 return factors[0];
103 }
104 let i = path_segment(times, pseudo_time);
105 let (t0, t1) = (times[i], times[i + 1]);
106 let (f0, f1) = (factors[i], factors[i + 1]);
107 if pseudo_time <= t0 {
108 f0
109 } else if pseudo_time >= t1 {
110 f1
111 } else {
112 f0 + (f1 - f0) * (pseudo_time - t0) / (t1 - t0)
113 }
114}
115
116fn path_slope(times: &[f64], factors: &[f64], pseudo_time: f64) -> f64 {
117 if times.len() == 1 {
118 return 0.0;
119 }
120 let i = path_segment(times, pseudo_time);
121 let (t0, t1) = (times[i], times[i + 1]);
122 if pseudo_time < t0 || pseudo_time > t1 {
123 return 0.0; }
125 (factors[i + 1] - factors[i]) / (t1 - t0)
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Default)]
140pub struct ElementLoad {
141 pub beam_uniform: [f64; 2],
144 pub body: [f64; 2],
146 pub edge_traction: [[f64; 2]; 4],
149 pub edge_pressure: [f64; 4],
152}
153
154impl ElementLoad {
155 pub fn uniform(wx: f64, wy: f64) -> Self {
157 ElementLoad {
158 beam_uniform: [wx, wy],
159 ..Default::default()
160 }
161 }
162
163 pub fn body(bx: f64, by: f64) -> Self {
165 ElementLoad {
166 body: [bx, by],
167 ..Default::default()
168 }
169 }
170
171 pub fn edge_traction(edge: usize, tx: f64, ty: f64) -> Self {
173 let mut load = ElementLoad::default();
174 load.edge_traction[edge] = [tx, ty];
175 load
176 }
177
178 pub fn edge_pressure(edge: usize, pressure: f64) -> Self {
180 let mut load = ElementLoad::default();
181 load.edge_pressure[edge] = pressure;
182 load
183 }
184
185 pub fn has_beam(&self) -> bool {
187 self.beam_uniform != [0.0; 2]
188 }
189
190 pub fn has_body(&self) -> bool {
192 self.body != [0.0; 2]
193 }
194
195 pub fn has_edge_from(&self, edges: usize) -> bool {
197 (edges..4).any(|k| self.edge_traction[k] != [0.0; 2] || self.edge_pressure[k] != 0.0)
198 }
199
200 pub fn has_edge(&self) -> bool {
202 self.has_edge_from(0)
203 }
204}
205
206impl std::ops::Add for ElementLoad {
207 type Output = ElementLoad;
208
209 fn add(self, other: ElementLoad) -> ElementLoad {
211 ElementLoad {
212 beam_uniform: std::array::from_fn(|i| self.beam_uniform[i] + other.beam_uniform[i]),
213 body: std::array::from_fn(|i| self.body[i] + other.body[i]),
214 edge_traction: std::array::from_fn(|k| {
215 std::array::from_fn(|i| self.edge_traction[k][i] + other.edge_traction[k][i])
216 }),
217 edge_pressure: std::array::from_fn(|k| self.edge_pressure[k] + other.edge_pressure[k]),
218 }
219 }
220}
221
222pub trait ElementLoadComponents {
225 const COUNT: usize;
228
229 fn component(&self, index: usize) -> f64;
232}
233
234impl ElementLoadComponents for ElementLoad {
237 const COUNT: usize = 16;
238
239 fn component(&self, index: usize) -> f64 {
240 match index {
241 0 | 1 => self.beam_uniform[index],
242 2 | 3 => self.body[index - 2],
243 4..=15 => {
244 let (edge, slot) = ((index - 4) / 3, (index - 4) % 3);
245 match slot {
246 0 | 1 => self.edge_traction[edge][slot],
247 _ => self.edge_pressure[edge],
248 }
249 }
250 _ => 0.0,
251 }
252 }
253}
254
255impl ElementLoadComponents for ElementLoad3 {
257 const COUNT: usize = 7;
258
259 fn component(&self, index: usize) -> f64 {
260 match index {
261 0..=2 => self.uniform[index],
262 3..=5 => self.body[index - 3],
263 6 => self.pressure,
264 _ => 0.0,
265 }
266 }
267}
268
269impl std::ops::Mul<f64> for ElementLoad {
270 type Output = ElementLoad;
271
272 fn mul(self, factor: f64) -> ElementLoad {
273 ElementLoad {
274 beam_uniform: self.beam_uniform.map(|v| v * factor),
275 body: self.body.map(|v| v * factor),
276 edge_traction: self.edge_traction.map(|t| t.map(|v| v * factor)),
277 edge_pressure: self.edge_pressure.map(|v| v * factor),
278 }
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Default)]
285pub struct ElementLoad3 {
286 pub uniform: [f64; 3],
290 pub body: [f64; 3],
294 pub pressure: f64,
297}
298
299impl ElementLoad3 {
300 pub const fn uniform(wx: f64, wy: f64, wz: f64) -> Self {
302 ElementLoad3 {
303 uniform: [wx, wy, wz],
304 body: [0.0; 3],
305 pressure: 0.0,
306 }
307 }
308
309 pub fn body(bx: f64, by: f64, bz: f64) -> Self {
311 ElementLoad3 {
312 body: [bx, by, bz],
313 ..Default::default()
314 }
315 }
316
317 pub fn pressure(pressure: f64) -> Self {
319 ElementLoad3 {
320 pressure,
321 ..Default::default()
322 }
323 }
324
325 pub fn has_uniform(&self) -> bool {
326 self.uniform != [0.0; 3]
327 }
328
329 pub fn has_body(&self) -> bool {
330 self.body != [0.0; 3]
331 }
332
333 pub fn has_pressure(&self) -> bool {
334 self.pressure != 0.0
335 }
336}
337
338impl std::ops::Add for ElementLoad3 {
339 type Output = ElementLoad3;
340
341 fn add(self, other: ElementLoad3) -> ElementLoad3 {
343 ElementLoad3 {
344 uniform: std::array::from_fn(|i| self.uniform[i] + other.uniform[i]),
345 body: std::array::from_fn(|i| self.body[i] + other.body[i]),
346 pressure: self.pressure + other.pressure,
347 }
348 }
349}
350
351impl std::ops::Mul<f64> for ElementLoad3 {
352 type Output = ElementLoad3;
353
354 fn mul(self, factor: f64) -> ElementLoad3 {
355 ElementLoad3 {
356 uniform: self.uniform.map(|v| v * factor),
357 body: self.body.map(|v| v * factor),
358 pressure: self.pressure * factor,
359 }
360 }
361}
362
363#[derive(Debug, Clone)]
376pub(crate) struct LoadPattern<
377 const NDOF: usize = NDF,
378 NId = NodeId,
379 EId = ElementId,
380 EL = ElementLoad,
381> where
382 NId: Eq + Hash + Copy,
383 EId: Eq + Hash + Copy,
384{
385 series: LoadSeries,
386 scale_factor: f64,
387 frozen_factor: Option<f64>,
393 nodal_loads: HashMap<NId, [f64; NDOF]>,
394 element_loads: HashMap<EId, EL>,
395}
396
397impl<const NDOF: usize, NId, EId, EL> LoadPattern<NDOF, NId, EId, EL>
398where
399 NId: Eq + Hash + Copy,
400 EId: Eq + Hash + Copy,
401 EL: Copy + std::ops::Add<Output = EL>,
402{
403 pub(crate) fn has_element_loads(&self) -> bool {
404 !self.element_loads.is_empty()
405 }
406
407 pub(crate) fn new(series: LoadSeries) -> Self {
408 LoadPattern {
409 series,
410 scale_factor: 1.0,
411 frozen_factor: None,
412 nodal_loads: HashMap::new(),
413 element_loads: HashMap::new(),
414 }
415 }
416
417 pub(crate) fn with_scale_factor(mut self, scale_factor: f64) -> Self {
418 self.scale_factor = scale_factor;
419 self
420 }
421
422 pub(crate) fn factor(&self, pseudo_time: f64) -> f64 {
423 self.frozen_factor
424 .unwrap_or_else(|| self.series.factor(pseudo_time) * self.scale_factor)
425 }
426
427 pub(crate) fn sensitivity(&self, pseudo_time: f64) -> f64 {
430 if self.frozen_factor.is_some() {
431 0.0
432 } else {
433 self.series.slope(pseudo_time) * self.scale_factor
434 }
435 }
436
437 pub(crate) fn is_unfrozen_path(&self) -> bool {
441 self.frozen_factor.is_none()
442 && matches!(
443 self.series,
444 LoadSeries::Path { .. } | LoadSeries::BoundedPath { .. }
445 )
446 }
447
448 pub(crate) fn hold_constant(&mut self, pseudo_time: f64) {
449 self.frozen_factor = Some(self.factor(pseudo_time));
450 }
451
452 pub(crate) fn add_nodal_load(&mut self, node: NId, dof: usize, value: f64) {
453 self.nodal_loads.entry(node).or_insert([0.0; NDOF])[dof] = value;
454 }
455
456 pub(crate) fn add_element_load(&mut self, element: EId, load: EL) {
459 self.element_loads
460 .entry(element)
461 .and_modify(|existing| *existing = *existing + load)
462 .or_insert(load);
463 }
464
465 pub(crate) fn nodal_load(&self, node: NId) -> Option<&[f64; NDOF]> {
466 self.nodal_loads.get(&node)
467 }
468
469 pub(crate) fn element_loads(&self) -> impl Iterator<Item = (&EId, &EL)> {
470 self.element_loads.iter()
471 }
472
473 pub(crate) fn element_load(&self, element: EId) -> Option<&EL> {
474 self.element_loads.get(&element)
475 }
476}
477
478pub(crate) fn active_element_patterns<'a, const NDOF: usize, NId, EId, EL>(
482 patterns: impl Iterator<Item = &'a LoadPattern<NDOF, NId, EId, EL>>,
483 pseudo_time: f64,
484) -> Vec<(f64, &'a LoadPattern<NDOF, NId, EId, EL>)>
485where
486 NId: Eq + Hash + Copy + 'a,
487 EId: Eq + Hash + Copy + 'a,
488 EL: Copy + std::ops::Add<Output = EL> + 'a,
489{
490 patterns
491 .filter(|pattern| pattern.has_element_loads())
492 .map(|pattern| (pattern.factor(pseudo_time), pattern))
493 .filter(|(factor, _)| *factor != 0.0)
494 .collect()
495}
496
497pub(crate) fn effective_element_load<const NDOF: usize, NId, EId, EL>(
502 active: &[(f64, &LoadPattern<NDOF, NId, EId, EL>)],
503 element: EId,
504) -> Option<EL>
505where
506 NId: Eq + Hash + Copy,
507 EId: Eq + Hash + Copy,
508 EL: Copy + std::ops::Add<Output = EL> + std::ops::Mul<f64, Output = EL>,
509{
510 let mut total: Option<EL> = None;
511 for (factor, pattern) in active {
512 if let Some(load) = pattern.element_load(element) {
513 let scaled = *load * *factor;
514 total = Some(match total {
515 Some(sum) => sum + scaled,
516 None => scaled,
517 });
518 }
519 }
520 total
521}
522
523#[cfg(test)]
524mod bounded_path_tests {
525 use super::LoadSeries;
526
527 #[test]
529 fn bounded_accelerogram_is_zero_outside_its_samples() {
530 for use_last in [false, true] {
531 let path = LoadSeries::BoundedPath {
532 times: vec![1.0, 2.0],
533 factors: vec![2.0, 4.0],
534 use_last,
535 };
536 assert_eq!(path.factor(0.0), 0.0);
537 assert_eq!(path.factor(1.0), 2.0);
538 assert_eq!(path.factor(1.5), 3.0);
539 assert_eq!(path.factor(2.0), 4.0);
540 assert_eq!(path.factor(3.0), if use_last { 4.0 } else { 0.0 });
541 assert_eq!(path.slope(0.0), 0.0);
542 assert_eq!(path.slope(1.5), 2.0);
543 assert_eq!(path.slope(3.0), 0.0);
544 }
545 }
546}