1use serde::{Deserialize, Serialize};
20
21use crate::error::MotorError;
22
23#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct MassElement {
27 pub mass_kg: f64,
29 pub cg_m: f64,
31 pub axial_inertia_kg_m2: f64,
33 pub transverse_inertia_kg_m2: f64,
35}
36
37impl MassElement {
38 pub const ZERO: Self = Self {
40 mass_kg: 0.0,
41 cg_m: 0.0,
42 axial_inertia_kg_m2: 0.0,
43 transverse_inertia_kg_m2: 0.0,
44 };
45
46 pub(crate) fn validate(&self, what: [&'static str; 4]) -> Result<(), MotorError> {
49 let checks = [
50 (self.mass_kg, what[0], true),
51 (self.cg_m, what[1], false),
52 (self.axial_inertia_kg_m2, what[2], true),
53 (self.transverse_inertia_kg_m2, what[3], true),
54 ];
55 for (value, name, non_negative) in checks {
56 if !value.is_finite() || (non_negative && value < 0.0) {
57 return Err(MotorError::Domain { what: name, value });
58 }
59 }
60 Ok(())
61 }
62
63 pub fn thin_tube(mass_kg: f64, cg_m: f64, radius_m: f64, length_m: f64) -> Self {
66 let r2 = radius_m * radius_m;
67 Self {
68 mass_kg,
69 cg_m,
70 axial_inertia_kg_m2: mass_kg * r2,
71 transverse_inertia_kg_m2: mass_kg * (0.5 * r2 + length_m * length_m / 12.0),
72 }
73 }
74
75 pub fn hollow_cylinder(
79 mass_kg: f64,
80 cg_m: f64,
81 outer_radius_m: f64,
82 inner_radius_m: f64,
83 length_m: f64,
84 ) -> Self {
85 let radii = outer_radius_m * outer_radius_m + inner_radius_m * inner_radius_m;
86 Self {
87 mass_kg,
88 cg_m,
89 axial_inertia_kg_m2: 0.5 * mass_kg * radii,
90 transverse_inertia_kg_m2: mass_kg * (0.25 * radii + length_m * length_m / 12.0),
91 }
92 }
93
94 pub fn combine<'a>(elements: impl IntoIterator<Item = &'a MassElement> + Clone) -> Self {
99 let mut mass = 0.0;
100 let mut moment = 0.0;
101 let mut axial = 0.0;
102 let mut count = 0.0;
103 let mut position_sum = 0.0;
104 for element in elements.clone() {
105 mass += element.mass_kg;
106 moment += element.mass_kg * element.cg_m;
107 axial += element.axial_inertia_kg_m2;
108 count += 1.0;
109 position_sum += element.cg_m;
110 }
111 let cg = if mass.is_nan() {
112 f64::NAN
113 } else if mass > 0.0 {
114 moment / mass
115 } else if count > 0.0 {
116 position_sum / count
117 } else {
118 0.0
119 };
120 let transverse = elements
121 .into_iter()
122 .map(|element| {
123 let arm = element.cg_m - cg;
124 element.transverse_inertia_kg_m2 + element.mass_kg * arm * arm
125 })
126 .sum();
127 Self {
128 mass_kg: mass,
129 cg_m: cg,
130 axial_inertia_kg_m2: axial,
131 transverse_inertia_kg_m2: transverse,
132 }
133 }
134
135 pub fn transverse_inertia_about(&self, z_m: f64) -> f64 {
138 let arm = self.cg_m - z_m;
139 self.transverse_inertia_kg_m2 + self.mass_kg * arm * arm
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn combining_matches_the_parallel_axis_theorem() {
149 let a = MassElement::hollow_cylinder(2.0, 0.1, 0.02, 0.01, 0.2);
150 let b = MassElement::thin_tube(1.0, 0.4, 0.03, 0.5);
151 let c = MassElement::combine([&a, &b]);
152 assert_eq!(c.mass_kg, 3.0);
153 assert!((c.cg_m - 0.2).abs() < 1e-15);
154 assert_eq!(
155 c.axial_inertia_kg_m2,
156 a.axial_inertia_kg_m2 + b.axial_inertia_kg_m2
157 );
158 let expected = a.transverse_inertia_kg_m2
159 + 2.0 * 0.1 * 0.1
160 + b.transverse_inertia_kg_m2
161 + 1.0 * 0.2 * 0.2;
162 assert!((c.transverse_inertia_kg_m2 - expected).abs() < 1e-15);
163 let whole = MassElement::hollow_cylinder(4.0, 0.5, 0.05, 0.02, 1.0);
165 let lower = MassElement::hollow_cylinder(2.0, 0.25, 0.05, 0.02, 0.5);
166 let upper = MassElement::hollow_cylinder(2.0, 0.75, 0.05, 0.02, 0.5);
167 let joined = MassElement::combine([&lower, &upper]);
168 assert!((joined.cg_m - whole.cg_m).abs() < 1e-15);
169 assert!((joined.axial_inertia_kg_m2 - whole.axial_inertia_kg_m2).abs() < 1e-15);
170 assert!((joined.transverse_inertia_kg_m2 - whole.transverse_inertia_kg_m2).abs() < 1e-15);
171 assert_eq!(
172 joined.transverse_inertia_about(0.0),
173 joined.transverse_inertia_kg_m2 + 4.0 * 0.25
174 );
175 }
176
177 #[test]
178 fn a_thin_tube_is_the_limit_of_a_hollow_cylinder() {
179 let tube = MassElement::thin_tube(1.0, 0.0, 0.05, 0.3);
180 let shell = MassElement::hollow_cylinder(1.0, 0.0, 0.05, 0.05, 0.3);
181 assert!((tube.axial_inertia_kg_m2 - shell.axial_inertia_kg_m2).abs() < 1e-15);
182 assert!((tube.transverse_inertia_kg_m2 - shell.transverse_inertia_kg_m2).abs() < 1e-15);
183 }
184
185 #[test]
186 fn massless_combinations_stay_finite() {
187 let none: [&MassElement; 0] = [];
188 assert_eq!(MassElement::combine(none), MassElement::ZERO);
189 let empty = MassElement {
190 cg_m: 0.3,
191 ..MassElement::ZERO
192 };
193 let other = MassElement {
194 cg_m: 0.5,
195 ..MassElement::ZERO
196 };
197 let c = MassElement::combine([&empty, &other]);
198 assert!((c.cg_m - 0.4).abs() < 1e-15);
199 assert_eq!(c.transverse_inertia_kg_m2, 0.0);
200 }
201}