1use geop_core_math::geop_error::{GeopError, GeopResult};
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum WireSize {
11 Awg { gauge: i32, insulation: f64 },
14 Diameter(f64),
16}
17
18pub const GAUGES: std::ops::RangeInclusive<i32> = 0..=40;
20
21impl WireSize {
22 pub fn diameter(&self) -> GeopResult<f64> {
26 let d = match *self {
27 WireSize::Awg { gauge, insulation } => {
28 if !GAUGES.contains(&gauge) {
29 return Err(GeopError::new(format!(
30 "AWG {gauge} is no gauge: gauges run from {} to {}",
31 GAUGES.start(),
32 GAUGES.end()
33 )));
34 }
35 if !(insulation >= 0.0 && insulation.is_finite()) {
36 return Err(GeopError::new(format!(
37 "an insulation {insulation} thick: it is a length, zero or more"
38 )));
39 }
40 0.127 * 92f64.powf((36 - gauge) as f64 / 39.0) + 2.0 * insulation
41 }
42 WireSize::Diameter(d) => d,
43 };
44 if d > 0.0 && d.is_finite() {
45 Ok(d)
46 } else {
47 Err(GeopError::new(format!(
48 "a wire {d} across: a diameter is more than zero"
49 )))
50 }
51 }
52
53 pub fn gauge(&self) -> Option<i32> {
55 match *self {
56 WireSize::Awg { gauge, .. } => Some(gauge),
57 WireSize::Diameter(_) => None,
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub struct Wire {
65 pub name: String,
66 pub size: WireSize,
67 pub colour: String,
69}
70
71impl Wire {
72 pub fn new(name: impl Into<String>) -> Self {
75 Wire {
76 name: name.into(),
77 size: WireSize::Awg {
78 gauge: 22,
79 insulation: 0.3,
80 },
81 colour: "#000000".into(),
82 }
83 }
84}
85
86pub fn bundle_diameter(wires: &[Wire], fill: f64) -> GeopResult<f64> {
90 if wires.is_empty() {
91 return Err(GeopError::new("the route has no wires: add one"));
92 }
93 if !(fill > 0.0 && fill <= 1.0) {
94 return Err(GeopError::new(format!(
95 "a fill factor of {fill}: it is the fraction of the bundle the wires fill, more than 0 and at most 1"
96 )));
97 }
98 let mut area = 0.0;
99 for wire in wires {
100 let d = wire
101 .size
102 .diameter()
103 .map_err(|e| e.with_context(format!("wire {:?}", wire.name)))?;
104 area += d * d;
105 }
106 Ok((area / fill).sqrt())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
116 fn gauges_give_diameters() {
117 let bare = |gauge| {
118 WireSize::Awg {
119 gauge,
120 insulation: 0.0,
121 }
122 .diameter()
123 .unwrap()
124 };
125 assert_eq!(bare(36), 0.127);
126 assert!((bare(22) - 0.6438).abs() < 1e-4, "{}", bare(22));
127 assert!((bare(16) / bare(22) - 2.0).abs() < 0.01);
128 let insulated = WireSize::Awg {
129 gauge: 22,
130 insulation: 0.3,
131 };
132 assert!((insulated.diameter().unwrap() - bare(22) - 0.6).abs() < 1e-12);
133 assert!(
134 WireSize::Awg {
135 gauge: 41,
136 insulation: 0.3
137 }
138 .diameter()
139 .is_err()
140 );
141 assert!(WireSize::Diameter(0.0).diameter().is_err());
142 }
143
144 #[test]
147 fn bundles_are_as_thick_as_their_wires_and_fill() {
148 let wires: Vec<Wire> = (0..4)
149 .map(|i| Wire {
150 size: WireSize::Diameter(1.5),
151 ..Wire::new(format!("w{i}"))
152 })
153 .collect();
154 let d = bundle_diameter(&wires, 0.5).unwrap();
155 assert!((d - 1.5 * 8f64.sqrt()).abs() < 1e-12, "{d}");
156 assert!(bundle_diameter(&[], 0.5).is_err());
157 assert!(bundle_diameter(&wires, 0.0).is_err());
158 assert!(bundle_diameter(&wires, 1.5).is_err());
159 }
160}