Resources Numerical accuracy Calculation policy

Overhead conductor sag and tension

Works out the load on a conductor in each load case, its tension by change of state on the ruling span, and the sag, length and support tensions of every span.

How the precision is kept

SI units and 64-bit numbers throughout. Each span is an exact catenary for the tension found, but the tension itself comes from the change-of-state equation on the ruling span, solved by Newton's method to a relative step of 1e-9, with tension limits met by bisection and repeated scaling. The starter library's conductor data are computed from rounded material values and then rounded again. The file header calls it a design aid.

Calculations under this policy

10 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

powerline/sag.js CivilCraft 0.8.1 64-bit 9 calculations Reviewed

Tests bin/test-powerline.mjs

Load per meter of conductor

64-bit

The vertical load (weight and ice), the wind load and their resultant per meter of conductor in one load case.

ice = π((D ÷ 2 + t)² − (D ÷ 2)²) × ρ × g; w_v = m·g + ice; w_h = wind pressure × (D + 2t); w = √(w_v² + w_h²)

Millimeters are divided by 1000 first. The ice area is the difference of two squares, formed by subtraction. The resultant comes from Math.hypot. Ice density defaults to 900 kg/m³ and g is 9.80665.

  • powerline/sag.js
  • lines 29 to 39
  • CivilCraft 0.8.1
export function conductorLoad(c, lc) {
  const D = (c.diameter_mm || 0) / 1000;
  const t = Math.max(0, (lc?.ice_mm || 0) / 1000);
  const rhoI = lc?.iceDensity > 0 ? lc.iceDensity : 900;
  const iceArea = Math.PI * (sq(D / 2 + t) - sq(D / 2));
  const iceN = iceArea * rhoI * G;
  const wv = (c.mass_kg_m || 0) * G + iceN;
  const dWind = D + (lc?.windIce === false ? 0 : 2 * t);
  const wh = Math.max(0, lc?.wind_Pa || 0) * dWind;
  return { wv, wh, w: Math.hypot(wv, wh), iceN, dWind };
}

Ruling span

64-bit

The equivalent span of a section between dead-ends.

L_r = √(Σ L³ ÷ Σ L)

Two sums of positive terms in span order and one square root; nothing cancels.

  • powerline/sag.js
  • lines 41 to 45
  • CivilCraft 0.8.1
export function rulingSpan(spans) {
  let s3 = 0, s1 = 0;
  for (const s of spans) { const L = Math.abs(s.L); s3 += L * L * L; s1 += L; }
  return s1 > 0 ? Math.sqrt(s3 / s1) : 0;
}

Axial stiffness and thermal expansion

64-bit

EA in newtons and the expansion coefficient per degree from the library's values.

EA = E (GPa) × 10⁹ × area (mm²) × 10⁻⁶; α = α₆ × 10⁻⁶

The unit factors are powers of ten applied by multiplication; 1e-6 is not exactly representable, so each product is rounded once in the last digit.

  • powerline/sag.js
  • lines 47 to 48
  • CivilCraft 0.8.1
const EA = (c) => (c.e_GPa || 0) * 1e9 * (c.area_mm2 || 0) * 1e-6;
const ALPHA = (c) => (c.alpha_e6 || 0) * 1e-6;

Change of state

64-bit

The horizontal tension in a new load case and temperature, from a known tension in another.

H³ + K·H² − M = 0; K = EA·(w₁²L² ÷ (24H₁²) + α(t₂ − t₁)) − H₁; M = EA·w₂²L² ÷ 24

Newton's method from max(H₁, 1), stopping when the step is under 1e-9 × max(1, H), so the root is good to about 9 digits, not 15. If 100 steps do not settle it, 200 halvings between 1e-6 and a doubled upper bound are used. K is a difference of terms scaled by EA, which can be much larger than H₁.

  • powerline/sag.js
  • lines 50 to 71
  • CivilCraft 0.8.1
export function changeOfState(c, L, from, to) {
  const ea = EA(c), al = ALPHA(c);
  if (!(ea > 0) || !(from.H > 0) || !(L > 0)) return NaN;
  const K = ea * (sq(from.w * L) / (24 * sq(from.H)) + al * (to.temp - from.temp)) - from.H;
  const M = ea * sq(to.w * L) / 24;
  // f(H) = H³ + K·H² − M has exactly one positive root when M > 0
  const f = (H) => H * H * H + K * H * H - M;
  let H = Math.max(from.H, 1);
  for (let i = 0; i < 100; i++) {
    const fx = f(H), d = 3 * H * H + 2 * K * H;
    if (!(d > 0)) { H *= 2; continue; }
    const n = H - fx / d;
    if (!(n > 0)) { H /= 2; continue; }
    if (Math.abs(n - H) < 1e-9 * Math.max(1, H)) return n;
    H = n;
  }
  // bisection fallback
  let lo = 1e-6, hi = Math.max(1, from.H);
  while (f(hi) < 0) hi *= 2;
  for (let i = 0; i < 200; i++) { const m = (lo + hi) / 2; if (f(m) > 0) hi = m; else lo = m; }
  return (lo + hi) / 2;
}

Catenary of one span

64-bit

The lowest point, sag, conductor length and the tension at each support of a span, level or inclined.

a = H ÷ w; x₀ = L ÷ 2 − a·asinh(dz ÷ (2a·sinh(L ÷ 2a))); y(x) = a·(cosh((x − x₀) ÷ a) − cosh(x₀ ÷ a)); length = a·(sinh((L − x₀) ÷ a) + sinh(x₀ ÷ a)); T = H·cosh(x ÷ a)

Heights are built from cosh(u) − 1, a subtraction of two nearly equal numbers when the span is short compared with a, which is the usual case for a tight conductor; the sag is then a further difference between the chord and the curve. The sag is taken where the curve is parallel to the chord, clamped to the span.

  • powerline/sag.js
  • lines 73 to 86
  • CivilCraft 0.8.1
export function catenary(span, H, w) {
  const L = Math.abs(span.L), dz = span.dz || 0;
  const a = H / w;
  // the lowest point's x (from the left end): dz = a·(cosh((L − x0)/a) − cosh(x0/a))
  const x0 = L / 2 - a * Math.asinh(dz / (2 * a * Math.sinh(L / (2 * a))));
  const y0 = -a * (Math.cosh(x0 / a) - 1);   // the lowest point's height below the left end
  const yAt = (x) => y0 + a * (Math.cosh((x - x0) / a) - 1);
  const xs = x0 + a * Math.asinh(dz / L);   // where the curve runs parallel to the chord
  const xm = Math.min(L, Math.max(0, xs));
  const sag = (dz * xm / L) - yAt(xm);
  const length = a * (Math.sinh((L - x0) / a) + Math.sinh(x0 / a));
  const Tleft = H * Math.cosh(x0 / a), Tright = H * Math.cosh((L - x0) / a);
  return { L, dz, a, x0, y0, xLow: x0, sag, length, Tleft, Tright, yAt };
}

Horizontal tension for a support-tension limit

64-bit

The horizontal tension at which the support tension of the level ruling span equals a limit.

find H with H·cosh(L_r·w ÷ 2H) = T

200 halvings of the interval from 1e-6 to T. H·cosh(L_r·w ÷ 2H) is not monotonic over that whole interval, since it grows without bound as H goes to zero, and the code does not check which root the halving ends on.

  • powerline/sag.js
  • lines 88 to 94
  • CivilCraft 0.8.1
/** Horizontal tension that gives a largest support tension T in the ruling span (level), by bisection on H. */
function hForT(T, w, Lr) {
  if (!(T > 0)) return NaN;
  let lo = 1e-6, hi = T;
  for (let i = 0; i < 200; i++) { const m = (lo + hi) / 2; if (m * Math.cosh(Lr / (2 * (m / w))) > T) hi = m; else lo = m; }
  return lo;
}

Meeting the tension limits on the real spans

64-bit

Lowers the reference tension until every support-tension limit holds on the actual spans, then reports which limits hold.

H ← H ÷ max(T_max ÷ limit); holds when value ≤ limit × (1 + 1e−6)

At most 20 passes, each one a full rebuild of every case; it stops when the worst ratio is within 1e-9 of 1. Dividing the tension by the ratio does not scale the support tension exactly, which is why more than one pass can be needed. The final check allows a relative slack of 1e-6.

  • powerline/sag.js
  • lines 135 to 154
  • CivilCraft 0.8.1
// a T limit is about the worst support of the real spans: scale down until every T limit holds (a few passes)
if (!fixed) {
  for (let pass = 0; pass < 20; pass++) {
    let worst = 1;
    for (const x of cands) {
      if (x.lim.kind !== 'T') continue;
      const N = x.lim.N > 0 ? x.lim.N : x.lim.pct / 100 * (c.rts_N || 0);
      const r = res[x.lc.id].Tmax / N;
      if (r > worst + 1e-9) { worst = r; gov = x; }
    }
    if (worst <= 1 + 1e-9) break;
    res = build(res[first.id].H / worst);
  }
}
// which limits hold
const checks = cands.map((x) => {
  const r = res[x.lc.id];
  const N = x.lim.fixed > 0 ? x.lim.fixed : x.lim.N > 0 ? x.lim.N : x.lim.pct / 100 * (c.rts_N || 0);
  const v = x.lim.kind === 'T' ? r.Tmax : r.H;
  return { caseId: x.lc.id, kind: x.lim.kind || 'H', limit: N, value: v, ok: x.lim.fixed > 0 || v <= N * (1 + 1e-6) };

Stringing table

64-bit

Tension, sag and conductor length of every span at each stringing temperature, in still air without ice.

per temperature: change of state from the reference, then a catenary per span

Each row is one change of state and one catenary per span, so it carries the Newton tolerance of 1e-9. The total length is the sum of the span lengths in order.

  • powerline/sag.js
  • lines 159 to 168
  • CivilCraft 0.8.1
export function stringingTable(c, spans, ref, temps) {
  const Lr = rulingSpan(spans);
  const w = conductorLoad(c, {}).w;
  return temps.map((temp) => {
    const H = changeOfState(c, Lr, ref, { w, temp });
    const sp = spans.map((s) => catenary(s, H, w));
    return { temp, H, Tmax: Math.max(...sp.map((x) => Math.max(x.Tleft, x.Tright))), length: sp.reduce((t, x) => t + x.length, 0),
      spans: sp.map((x) => ({ L: x.L, dz: x.dz, sag: x.sag, length: x.length, Tleft: x.Tleft, Tright: x.Tright })) };
  });
}

Unbalanced ice load at a suspension support

64-bit

The longitudinal pull on a support when one adjacent span is iced and the other is bare.

dT = max(|H iced, left − H bare, right|, |H iced, right − H bare, left|)

Each span is solved alone, with its own length in place of the ruling span, which the code comment calls conservative. The result is a difference of two tensions from separate Newton solves.

  • powerline/sag.js
  • lines 170 to 184
  • CivilCraft 0.8.1
/** Each span on its own from the section's reference: tension with ice vs bare, at the ice case's temperature. */
export function unbalancedIce(c, spans, ref, iceCase, bareCase) {
  const wi = conductorLoad(c, iceCase).w, wb = conductorLoad(c, bareCase || { temp: iceCase.temp }).w;
  const per = spans.map((s) => {
    const Hi = changeOfState(c, Math.abs(s.L), ref, { w: wi, temp: iceCase.temp });
    const Hb = changeOfState(c, Math.abs(s.L), ref, { w: wb, temp: (bareCase || iceCase).temp });
    return { Hi, Hb };
  });
  const out = [];
  for (let i = 1; i < spans.length; i++) {
    const a = per[i - 1], b = per[i];
    out.push({ support: i, dT: Math.max(Math.abs(a.Hi - b.Hb), Math.abs(b.Hi - a.Hb)) });
  }
  return out;
}
powerline/library.js CivilCraft 0.8.1 64-bit 1 calculation Reviewed

Tests bin/test-powerline.mjs, bin/test-powerline-net.mjs, bin/test-powerline-out.mjs, bin/test-powerline-don.mjs

Starter library conductor data

64-bit

The area, diameter, mass, strength, stiffness, expansion and resistance of the generic stranded conductors that come with the app.

mass = 1.02 × Σ area × density; strength = 0.9 × Σ area × wire strength; R₂₀ = 1.02 × resistivity ÷ area × 1000

Computed from textbook material values that the code comment calls rounded, with a lay factor of 1.02 and a stranding loss of 10 percent. The results are then rounded before use: area to 1 decimal, diameter to 2, mass to 4, strength to a whole newton, modulus and expansion to 1 decimal, resistance to 4 decimals.

  • powerline/library.js
  • lines 260 to 274
  • CivilCraft 0.8.1
function stranded({ key, name, category, wires, d, steelWires = 0, ds = 0, mat = 'al' }) {
  const M = { al: { rho: 2703, res: 0.028264, uts: 170e6, e: 59, a: 23 }, alloy: { rho: 2690, res: 0.0328, uts: 300e6, e: 59, a: 23 }, cu: { rho: 8890, res: 0.017241, uts: 400e6, e: 105, a: 17 } }[mat];
  const aA = wires * PI * d * d / 4, aS = steelWires * PI * ds * ds / 4;   // mm²
  const layers = (n) => (n <= 1 ? 1 : n <= 7 ? 3 : n <= 19 ? 5 : n <= 37 ? 7 : 9);
  const D = steelWires ? ds * layers(steelWires) + 2 * d * ((layers(wires + steelWires) - layers(steelWires)) / 2) : d * layers(wires);
  const mass = 1.02 * (aA * 1e-6 * M.rho + aS * 1e-6 * 7780);
  const rts = 0.9 * (aA * 1e-6 * M.uts + aS * 1e-6 * 1300e6);
  const e = steelWires ? (M.e * aA + 200 * aS) / (aA + aS) : M.e;
  const alpha = steelWires ? (M.a * M.e * aA + 11.5 * 200 * aS) / (M.e * aA + 200 * aS) : M.a;
  const r = 1.02 * M.res / aA * 1000;
  return { key, kind: 'conductor', source: 'starter', name, category, note: t('pl.st.note'),
    material: steelWires ? 'aluminium-steel' : mat === 'alloy' ? 'aluminium-alloy' : mat === 'cu' ? 'copper' : 'aluminium',
    area_mm2: +(aA + aS).toFixed(1), diameter_mm: +D.toFixed(2), mass_kg_m: +mass.toFixed(4), rts_N: Math.round(rts), e_GPa: +e.toFixed(1),
    alpha_e6: +alpha.toFixed(1), r20_ohm_km: +r.toFixed(4) };
}

In detail

Method
Catenary per span; change of state on the ruling span, H₂²·(H₂ − H₁ + EA·w₁²L² ÷ (24H₁²) + EA·α·(t₂ − t₁)) = EA·w₂²L² ÷ 24, as the file header writes it. Ruling span √(ΣL³ ÷ ΣL). Ice as a ring of the given radial thickness and density.
Number types
64-bit numbers. g = 9.80665 from units.js.
Tolerances
Newton stops when the step is below 1e-9 × max(1, H), at most 100 steps, then 200 halvings as a fallback. Tension-limit bisection: 200 halvings. Limit scaling: up to 20 passes, to a ratio of 1 + 1e-9. A limit counts as held within a relative 1e-6.
Limits
The change-of-state equation uses the w²L² ÷ 24H² slack term on the ruling span, the usual approximation; spans of very different length or slope in one section are covered only through the ruling span. Unbalanced ice treats each span on its own.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach