Skip to main content

geop_ops_harness/
wire.rs

1//! [`Wire`]s, and the bundle they make: how thick each is, by its gauge or
2//! its diameter, and how thick they are together.
3
4use geop_core_math::geop_error::{GeopError, GeopResult};
5use serde::{Deserialize, Serialize};
6
7/// How thick a wire is.
8#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum WireSize {
11    /// By its American wire gauge — `0` to `40`, the conductor's — and the
12    /// wall of insulation round it.
13    Awg { gauge: i32, insulation: f64 },
14    /// By its outer diameter, insulation included.
15    Diameter(f64),
16}
17
18/// The gauges [`WireSize::Awg`] takes.
19pub const GAUGES: std::ops::RangeInclusive<i32> = 0..=40;
20
21impl WireSize {
22    /// The outer diameter, insulation included. The conductor of gauge `n`
23    /// is `0.127 mm × 92^((36 − n) / 39)` across, which is what defines the
24    /// gauges.
25    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    /// Its gauge, if it is given by one.
54    pub fn gauge(&self) -> Option<i32> {
55        match *self {
56            WireSize::Awg { gauge, .. } => Some(gauge),
57            WireSize::Diameter(_) => None,
58        }
59    }
60}
61
62/// A wire of a route: what it is called, how thick it is, and its colour.
63#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64pub struct Wire {
65    pub name: String,
66    pub size: WireSize,
67    /// `#rrggbb`.
68    pub colour: String,
69}
70
71impl Wire {
72    /// A wire of gauge 22 with a wall of 0.3 — common hookup wire, about
73    /// 1.2 mm across — black.
74    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
86/// How thick `wires` are, bundled: the diameter of a circle whose area is
87/// theirs over `fill`, the fraction of a bundle's section its wires fill —
88/// round wires never fill it all — `sqrt(sum d² / fill)`.
89pub 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    /// Gauges are what they are defined as: 36 is 0.127 across, every six
114    /// gauges down doubles it, roughly; 22 is 0.644.
115    #[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    /// Four wires of one diameter, filling half the bundle, make a bundle
145    /// `sqrt(4 / 0.5) = 2 sqrt 2` of them across.
146    #[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}