Skip to main content

geop_ops_hole/
pipe.rs

1//! American National Standard taper pipe threads, NPT (ASME B1.20.1), from
2//! 1/16 to 2 inch, in millimetres: a 60° thread on a cone tapering 1 in 16
3//! on the diameter, so that its radius changes by [`TAPER`] per unit of
4//! length.
5//!
6//! Each size is given by its threads per inch, the pipe's outside diameter,
7//! the pitch diameter `E0` at the small end of the external thread (the
8//! pipe's end), the hand-tight engagement `L1` and the effective thread
9//! length `L2`. An internal thread is cut so that a pipe screwed in by hand
10//! reaches `L1` in: its pitch diameter at the face it is tapped from is
11//! `E1 = E0 + L1 / 16`. The thread is `h = 0.8 P` high, truncated equally at
12//! its crest and its root from the sharp V of height `H = 0.866 P`, so its
13//! major and minor diameters are `h` either side of the pitch diameter.
14//!
15//! A tapped hole for it is reamed to the taper, its diameter at the face the
16//! minor diameter there, `E1 - h`.
17
18use geop_core_math::geop_error::{GeopError, GeopResult};
19
20use crate::iso::TRIANGLE_HEIGHT;
21
22/// How much an NPT thread's radius changes per unit of length: half its
23/// taper of 1 in 16 on the diameter.
24pub const TAPER: f64 = 1.0 / 32.0;
25
26/// Millimetres per inch.
27const INCH: f64 = 25.4;
28
29/// One size of NPT.
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct PipeSize {
32    /// `1/2`: the nominal pipe size, in inches.
33    pub name: &'static str,
34    pub threads_per_inch: f64,
35    /// The pipe's outside diameter.
36    pub outside: f64,
37    /// The pitch diameter at the small end of the external thread.
38    pub e0: f64,
39    /// The hand-tight engagement.
40    pub l1: f64,
41    /// The effective thread length of the external thread.
42    pub l2: f64,
43}
44
45impl PipeSize {
46    pub fn pitch(&self) -> f64 {
47        INCH / self.threads_per_inch
48    }
49
50    /// The thread's height, from its root to its crest: `0.8 P`.
51    pub fn height(&self) -> f64 {
52        0.8 * self.pitch()
53    }
54
55    /// The pitch diameter at the large end of the internal thread, where a
56    /// hole is tapped from: `E1 = E0 + L1 / 16`.
57    pub fn e1(&self) -> f64 {
58        self.e0 + 2.0 * TAPER * self.l1
59    }
60
61    /// The major and minor diameters where a thread starts: an internal
62    /// one at its large end, the face a hole is tapped from (`E1`); an
63    /// external one at its small end, the pipe's end (`E0`).
64    pub fn diameters(&self, internal: bool) -> (f64, f64) {
65        let pitch_diameter = if internal { self.e1() } else { self.e0 };
66        (
67            pitch_diameter + self.height(),
68            pitch_diameter - self.height(),
69        )
70    }
71
72    /// How far the sharp V's crest lies beyond the major diameter, in
73    /// radius: half of what the truncation takes off.
74    pub fn crest(&self) -> f64 {
75        (TRIANGLE_HEIGHT * self.pitch() - self.height()) / 2.0
76    }
77
78    /// How the thread is called: `1/2-14 NPT`.
79    pub fn designation(&self) -> String {
80        format!("{}-{} NPT", self.name, self.threads_per_inch)
81    }
82}
83
84const fn size(
85    name: &'static str,
86    threads_per_inch: f64,
87    outside: f64,
88    e0: f64,
89    l1: f64,
90    l2: f64,
91) -> PipeSize {
92    PipeSize {
93        name,
94        threads_per_inch,
95        outside: outside * INCH,
96        e0: e0 * INCH,
97        l1: l1 * INCH,
98        l2: l2 * INCH,
99    }
100}
101
102/// The sizes of ASME B1.20.1 Table 2 from 1/16 to 2, smallest first.
103pub const SIZES: [PipeSize; 10] = [
104    size("1/16", 27.0, 0.3125, 0.27118, 0.160, 0.2611),
105    size("1/8", 27.0, 0.405, 0.36351, 0.1615, 0.2639),
106    size("1/4", 18.0, 0.540, 0.47739, 0.2278, 0.4018),
107    size("3/8", 18.0, 0.675, 0.61201, 0.240, 0.4078),
108    size("1/2", 14.0, 0.840, 0.75843, 0.320, 0.5337),
109    size("3/4", 14.0, 1.050, 0.96768, 0.339, 0.5457),
110    size("1", 11.5, 1.315, 1.21363, 0.400, 0.6828),
111    size("1-1/4", 11.5, 1.660, 1.55713, 0.420, 0.7068),
112    size("1-1/2", 11.5, 1.900, 1.79609, 0.420, 0.7235),
113    size("2", 11.5, 2.375, 2.26902, 0.436, 0.7565),
114];
115
116/// The size called `name`: `1/2`.
117pub fn npt(name: &str) -> GeopResult<&'static PipeSize> {
118    SIZES.iter().find(|s| s.name == name).ok_or_else(|| {
119        let known: Vec<&str> = SIZES.iter().map(|s| s.name).collect();
120        GeopError::new(format!(
121            "{name:?} is no NPT pipe size; there are {}",
122            known.join(", ")
123        ))
124    })
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    /// The table holds together: the thread's major diameter at the pipe's
132    /// end lies below the pipe's outside diameter, and the hand-tight plane
133    /// within the effective length; sizes grow.
134    #[test]
135    fn the_table_is_consistent() {
136        for s in &SIZES {
137            let (major, minor) = s.diameters(false);
138            assert!(minor < s.e0 && s.e0 < major, "{s:?}");
139            assert!(major < s.outside, "{s:?}");
140            assert!(s.l1 < s.l2, "{s:?}");
141            assert!(s.diameters(true).0 > major, "{s:?}");
142            assert!(s.crest() > 0.0, "{s:?}");
143        }
144        for w in SIZES.windows(2) {
145            assert!(w[0].outside < w[1].outside, "{w:?}");
146        }
147        let half = npt("1/2").unwrap();
148        assert_eq!(half.designation(), "1/2-14 NPT");
149        // E1 of 1/2 NPT is 0.77843 in.
150        assert!((half.e1() / INCH - 0.77843).abs() < 1e-9);
151        assert!(npt("5/8").is_err());
152    }
153}