Resources Numerical accuracy Calculation policy

Vertical profile: grades and parabolic curves

Works out the design elevation and grade at any station from the PVIs and their vertical curves, and samples the existing ground along the alignment.

How the precision is kept

Grades are rise over run between PVIs. A vertical curve is a symmetric parabola evaluated in 64-bit from the curve's start, so the terms stay small. Station comparisons use an absolute tolerance of 1e-9, and two PVIs closer than 1e-6 are reported as an error.

Calculations under this policy

4 calculations in 1 source file. 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.

profile.js CivilCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-civilcraft.mjs

Grades and vertical curves

64-bit

Builds the grades between PVIs and the curve at each interior PVI.

g = Δz ÷ Δsta; A = (g₂ − g₁) × 100; L = K × |A|; z_BVC = z_PVI − g₁ × L ÷ 2; x_high/low = −g₁ × L ÷ (g₂ − g₁)

Grades divide by the station difference; two PVIs less than 1e-6 apart are reported, but the division is still done. A curve is skipped when L ≤ 0 or |A| < 1e-9. The high or low point is computed only when the grades have opposite signs, so its divisor cannot be zero. Overlap tests use 1e-9, absolute.

  • profile.js
  • lines 34 to 64
  • CivilCraft 0.8.1
export function buildProfile(pvisIn) {
  const errors = [];
  const pvis = (pvisIn || []).filter((p) => Number.isFinite(p.sta) && Number.isFinite(p.z)).map((p) => ({ ...p })).sort((a, b) => a.sta - b.sta);
  for (let i = 1; i < pvis.length; i++) if (pvis[i].sta - pvis[i - 1].sta < 1e-6) errors.push(t('prof.err.two', { sta: pvis[i].sta }));
  const grades = [];
  for (let i = 0; i + 1 < pvis.length; i++) grades.push({ from: pvis[i].sta, to: pvis[i + 1].sta, g: (pvis[i + 1].z - pvis[i].z) / (pvis[i + 1].sta - pvis[i].sta) });
  const curves = [];
  for (let i = 1; i + 1 < pvis.length; i++) {
    const v = pvis[i], g1 = grades[i - 1].g, g2 = grades[i].g, A = (g2 - g1) * 100;
    let L = Number(v.L) || 0;
    if (!L && Number(v.K) > 0) L = Number(v.K) * Math.abs(A);
    v.L = L;
    if (L <= 0 || Math.abs(A) < 1e-9) continue;
    const bvc = v.sta - L / 2, evc = v.sta + L / 2;
    const c = { i, pvi: v.sta, zPvi: v.z, bvc, evc, L, g1, g2, A, K: L / Math.abs(A), zBvc: v.z - g1 * L / 2, zEvc: v.z + g2 * L / 2, kind: g2 < g1 ? 'crest' : 'sag' };
    if (g1 * g2 < 0) {
      const x = -g1 * L / (g2 - g1);
      const pt = { sta: bvc + x, z: c.zBvc + g1 * x + (g2 - g1) * x * x / (2 * L) };
      if (c.kind === 'crest') c.high = pt; else c.low = pt;
    }
    curves.push(c);
  }
  // curves must stay between their neighbours and not overlap
  for (let k = 0; k < curves.length; k++) {
    const c = curves[k], prevEnd = k ? curves[k - 1].evc : pvis[0].sta;
    const left = pvis[c.i - 1].sta, right = pvis[c.i + 1].sta;
    if (c.bvc < left - 1e-9 || c.bvc < prevEnd - 1e-9) errors.push(t('prof.err.before', { pvi: c.i + 1, len: c.L.toFixed(2) }));
    if (c.evc > right + 1e-9) errors.push(t('prof.err.past', { pvi: c.i + 1, len: c.L.toFixed(2) }));
  }
  return new Profile(pvis, grades, curves, errors);
}

Design elevation at a station

64-bit

The profile's elevation at any station.

z = z_BVC + g₁ × x + (g₂ − g₁) × x² ÷ (2L), x = sta − BVC; on a tangent z = zᵢ + g × (sta − staᵢ)

x is measured from the curve's start, and the three terms are added in order. More than 1e-9 outside the profile the result is null.

  • profile.js
  • lines 74 to 80
  • CivilCraft 0.8.1
elevationAt(sta) {
  const P = this.pvis;
  if (P.length < 2 || sta < this.start - 1e-9 || sta > this.end + 1e-9) return null;
  for (const c of this.curves) if (sta >= c.bvc && sta <= c.evc) { const x = sta - c.bvc; return c.zBvc + c.g1 * x + (c.g2 - c.g1) * x * x / (2 * c.L); }
  for (let i = 0; i + 1 < P.length; i++) if (sta <= P[i + 1].sta + 1e-9) return P[i].z + this.grades[i].g * (sta - P[i].sta);
  return null;
}

Grade at a station

64-bit

The profile's slope at any station.

g = g₁ + (g₂ − g₁) × (sta − BVC) ÷ L

Linear between the two grades across a curve. On a tangent the stored grade is returned.

  • profile.js
  • lines 81 to 85
  • CivilCraft 0.8.1
gradeAt(sta) {
  for (const c of this.curves) if (sta >= c.bvc && sta <= c.evc) return c.g1 + (c.g2 - c.g1) * (sta - c.bvc) / c.L;
  for (const g of this.grades) if (sta <= g.to + 1e-9) return g.g;
  return null;
}

Existing ground along the alignment

64-bit

Samples the surface's elevation at regular stations and at the alignment's key stations.

step = length ÷ 1000, at least 0.5; stations: start, start + step, …, end, plus key stations

Stations are built by adding the step repeatedly, and each is rounded to 6 decimals with toFixed(6). The ground elevation is looked up at each station's point with a tolerance of 1e-6.

  • profile.js
  • lines 21 to 32
  • CivilCraft 0.8.1
export function sampleSurface(surface, alignment, { step } = {}) {
  if (!surface || !alignment?.ok) return [];
  const st = step > 0 ? step : Math.max(0.5, alignment.length / 1000);
  const stas = new Set();
  for (let s = alignment.start; s < alignment.end; s += st) stas.add(Number(s.toFixed(6)));
  stas.add(alignment.end);
  for (const k of alignment.keyStations()) stas.add(k.sta);
  return [...stas].sort((a, b) => a - b).map((sta) => {
    const p = alignment.pointAt(sta);
    return { sta, z: surface.elevationAt(p.x, p.y, 1e-6) };
  });
}

In detail

Method
z = z_BVC + g₁ × x + (g₂ − g₁) × x² ÷ (2L) on a curve from BVC (x = 0) to EVC (x = L); the high or low point is at x = −g₁ × L ÷ (g₂ − g₁), as the file's header states.
Number types
64-bit numbers.
Tolerances
1e-9 in stations for range and overlap tests; 1e-6 for two PVIs at one station; a grade change below 1e-9 percent gives no curve; ground samples use 1e-6 as the surface lookup tolerance.
Limits
The tolerances are absolute, not scaled to the size of the stations. Ground sample stations are rounded to 6 decimals.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach