The seismic base shear and story forces by the equivalent lateral force procedure, the building's period, the weight of each floor and the drift check.
How the precision is kept
The procedure's formulas are evaluated in 64-bit numbers with the coefficients the standard prints. The approximate period is an empirical formula and the factor Cu a table read by straight lines, so those set the accuracy. Heights are in millimeters and converted to meters where the formula needs them.
Calculations under this policy
11 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.
The building's approximate fundamental period from its height and structural system.
Ta = Ct × (hn ÷ 1000)^x, hn in mm
Ct and x are the SI values of the standard's table, printed to three significant digits (0.0724, 0.0466, 0.0731, 0.0488). The formula is empirical.
frame/loads-asce7.js
lines 40 to 43
CadCraft 0.8.2
export function approxPeriod(system, hn) {
const s = PERIOD_SYSTEMS[system] || PERIOD_SYSTEMS.other;
return s.Ct * Math.pow(hn / 1000, s.x);
}
Upper limit factor on the period
64-bit
The factor Cu that caps a period taken from analysis.
table of SD1 against Cu, straight lines between rows
Linear interpolation between five table rows; 1.7 at or below SD1 = 0.1 and 1.4 at or above 0.3.
frame/loads-asce7.js
lines 44 to 50
CadCraft 0.8.2
export function cuFactor(SD1) {
const t = [[0.1, 1.7], [0.15, 1.6], [0.2, 1.5], [0.3, 1.4], [0.4, 1.4]];
if (SD1 <= t[0][0]) return t[0][1];
if (SD1 >= t[4][0]) return 1.4;
for (let k = 1; k < t.length; k++) if (SD1 <= t[k][0]) { const [a, ca] = t[k - 1], [b, cb] = t[k]; return ca + (cb - ca) * (SD1 - a) / (b - a); }
return 1.4;
}
Seismic response coefficient
64-bit
The coefficient Cs that multiplies the building's weight to give the base shear.
Cs = SDS ÷ (R ÷ Ie), at most SD1 ÷ (T R ÷ Ie) or SD1 TL ÷ (T² R ÷ Ie), at least max(0.044 SDS Ie, 0.01) and 0.5 S1 ÷ (R ÷ Ie) where S1 ≥ 0.6
Plain 64-bit divisions and comparisons. The division by the period is not guarded here.
frame/loads-asce7.js
lines 53 to 63
CadCraft 0.8.2
export function seismicCs({ SDS, SD1, S1 = 0, R, Ie = 1, T, TL = 8 }) {
const RI = R / Ie;
const raw = SDS / RI;
const max = T <= TL ? SD1 / (T * RI) : SD1 * TL / (T * T * RI);
let min = Math.max(0.044 * SDS * Ie, 0.01), minEq = '12.8-5';
if (S1 >= 0.6 && 0.5 * S1 / RI > min) { min = 0.5 * S1 / RI; minEq = '12.8-6'; }
let Cs = raw, eq = '12.8-2';
if (max < Cs) { Cs = max; eq = T <= TL ? '12.8-3' : '12.8-4'; }
if (min > Cs) { Cs = min; eq = minEq; }
return { Cs, eq, raw, max, min };
}
Height exponent
64-bit
The exponent k of the vertical distribution of the story forces.
k = 1 for T ≤ 0.5 s; 2 for T ≥ 2.5 s; 1 + (T − 0.5) ÷ 2 between
One linear expression between the two limits.
frame/loads-asce7.js
line 64
CadCraft 0.8.2
export function exponentK(T) { return T <= 0.5 ? 1 : T >= 2.5 ? 2 : 1 + (T - 0.5) / 2; }
Base shear and story forces
64-bit
The base shear, the force on each floor and the shear in each story.
V = Cs W; Cvx = wx hx^k ÷ Σ wi hi^k; Fx = Cvx V; Vx = Σ F from the top down
Weights are added from the lowest floor up and story shears from the top down, in 64-bit. Heights are divided by 1000 before the power, so the exponent acts on meters.
frame/loads-asce7.js
lines 66 to 80
CadCraft 0.8.2
export function elf({ levels, base = 0, SDS, SD1, S1 = 0, R, Ie = 1, T = null, TL = 8, system = 'other' }) {
const lv = [...levels].sort((a, b) => a.z - b.z);
const hn = lv[lv.length - 1].z - base;
const Ta = approxPeriod(system, hn);
const Tu = periodUsed({ T, Ta, SD1 });
const cs = seismicCs({ SDS, SD1, S1, R, Ie, T: Tu, TL });
const W = lv.reduce((s, l) => s + l.w, 0), V = cs.Cs * W;
const k = exponentK(Tu);
const sum = lv.reduce((s, l) => s + l.w * Math.pow((l.z - base) / 1000, k), 0);
const out = lv.map((l) => { const Cvx = l.w * Math.pow((l.z - base) / 1000, k) / sum; return { id: l.id, z: l.z, hx: l.z - base, w: l.w, Cvx, Fx: Cvx * V }; });
let shear = 0;
for (let i = out.length - 1; i >= 0; i--) { shear += out[i].Fx; out[i].Vx = shear; }
return { V, W, Cs: cs.Cs, csEq: cs.eq, cs, T: Tu, Ta, Cu: cuFactor(SD1), k, hn, levels: out,
clauses: ['ASCE/SEI 7-22 12.8.1', 'ASCE/SEI 7-22 12.8.1.1 (' + cs.eq + ')', 'ASCE/SEI 7-22 12.8.2.1 (12.8-8, Table 12.8-2)', 'ASCE/SEI 7-22 12.8.3'] };
}
Story force on a rigid floor
64-bit
Puts a story force on a floor at its master point, with the accidental torsion.
Mz = −((x₀ − x_m) Fy − (y₀ − y_m) Fx) ± 0.05 × plan size × F
The force is applied at one node with the moment that moves it to the master point. The plan size is the extent of the floor's nodes square to the force, a maximum minus a minimum.
frame/loads-asce7.js
lines 82 to 103
CadCraft 0.8.2
/** A load case putting storey forces on the floors (rigid diaphragms) of a clean frame model. */
export function floorCase(model, { id, name, kind = 'seismic', dir = 'X', forces, ecc = 0.05, sign = 1 }) {
const at = new Map(model.nodes.map((n) => [n.id, n]));
const sup = new Set(model.supports.filter((s) => s.fix[0] || s.fix[1] || s.fix[5] || s.k?.[0] || s.k?.[1] || s.k?.[5]).map((s) => s.node));
const loads = [];
for (const d of model.diaphragms || []) {
const F = forces[d.id];
if (!F) continue;
const nodes = d.nodes.filter((n) => !sup.has(n)).map((n) => at.get(n)); // as plate-solve.js: held nodes leave the floor
if (!nodes.length) continue;
const xm = d.master ? d.master.x : nodes.reduce((s, n) => s + n.x, 0) / nodes.length;
const ym = d.master ? d.master.y : nodes.reduce((s, n) => s + n.y, 0) / nodes.length;
const xs = nodes.map((n) => n.x), ys = nodes.map((n) => n.y);
const across = dir === 'X' ? Math.max(...ys) - Math.min(...ys) : Math.max(...xs) - Math.min(...xs);
const n0 = nodes[0];
const Fx = dir === 'X' ? F : 0, Fy = dir === 'Y' ? F : 0;
// F at n0 makes a moment (x0 − xm) Fy − (y0 − ym) Fx about the master; take it back, then add the accidental torsion
const Mz = -((n0.x - xm) * Fy - (n0.y - ym) * Fx) + sign * ecc * across * F;
loads.push({ type: 'node', node: n0.id, F: [Fx, Fy, 0, 0, 0, Mz] });
}
return { id, name: name || id, kind, selfWeight: 0, loads };
}
Design drift against the allowable
64-bit
The design story drift and its ratio to the allowable drift.
δ = Cd δ_e ÷ Ie; allowed = 0.020, 0.015 or 0.010 × story height; ratio = δ ÷ allowed
Two products and a division. The division by the allowable drift is not guarded against a story height of 0.
The heights at which level members meet, offered as the building's floors.
z = (z_a + z_b) ÷ 2 of a level member; levels within 1 mm are one; stored z rounded to 0.1 mm
The height kept in the analysis settings is rounded to 0.1 mm. A member counts as level when its ends differ by at most 1 mm in height; both limits are absolute.
frame/frame-concrete.js
lines 108 to 121
CadCraft 0.8.2
/** Heights where at least two beam/brace ends meet (above the lowest support): the floors a plan would have. */
export function suggestFloors(model) {
const at = new Map(model.nodes.map((q) => [q.id, q]));
const base = Math.min(...model.supports.map((s) => at.get(s.node)?.z ?? 0), Infinity);
const levels = [];
for (const m of model.members) {
const a = at.get(m.i), b = at.get(m.j);
if (Math.abs(a.z - b.z) > 1) continue; // only members lying in a level
const z = (a.z + b.z) / 2, hit = levels.find((l) => Math.abs(l.z - z) <= 1);
if (hit) hit.nodes.add(m.i).add(m.j); else levels.push({ z, nodes: new Set([m.i, m.j]) });
}
return levels.filter((l) => Number.isFinite(base) && l.z > base + 1 && l.nodes.size >= 2).sort((p, q) => p.z - q.z)
.map((l, k) => ({ id: 'F' + (k + 1), name: 'F' + (k + 1), z: Math.round(l.z * 10) / 10, w: 0 }));
}
Floor weights into newtons
64-bit
Turns the floor weights typed in kN into the N the procedure works in.
w (N) = w (kN) × 1000
One multiplication by 1000 in 64-bit; the weights are then added in newtons.
frame/frame-concrete.js
lines 144 to 148
CadCraft 0.8.2
const levels = floors.filter((f) => f.w > 0).map((f) => ({ id: f.id, z: f.z, w: f.w * 1000 }));
const dirs = s.dir === 'both' ? ['X', 'Y'] : [s.dir];
// one ELF per direction: the period from the analysis can differ between X and Y (12.8.2: T never above Cu·Ta)
const elfBy = {};
for (const dir of dirs) elfBy[dir] = elf({ levels, base: Math.min(...sup), SDS: s.SDS, SD1: s.SD1, S1: s.S1, R: s.R, Ie: s.Ie, T: s.useT ? (dir === 'X' ? s.Tx : s.Ty) : null, TL: s.TL, system: s.system });
Period of the analyzed frame
64-bit
The fundamental period in each direction from the floor weights and the frame's displacements under the seismic forces.
T = 2π √(Σ wᵢ δᵢ² ÷ (g Σ fᵢ δᵢ))
Each floor's displacement is the running sum of the story drifts from the base, converted from mm to m. g is 9.80665 m/s². A direction is skipped when either sum is not positive.
frame/frame-concrete.js
lines 169 to 198
CadCraft 0.8.2
export function rayleighPeriods(frame, sol, concrete) {
const c = cleanConcrete(concrete);
if (!c || !sol?.ok || !frame?.model?.diaphragms?.length) return { ok: false, error: 'no-cases' };
const out = { ok: false };
const model = frame.model, floorOf = new Map();
for (const d of model.diaphragms) for (const nd of d.nodes || []) floorOf.set(nd, d.id);
let weights = false;
for (const dir of ['X', 'Y']) {
const R = sol.results['case:E' + dir + '1'], cs = model.cases.find((q) => q.id === 'E' + dir + '1');
if (!R || !cs) continue;
const idx = dir === 'X' ? 0 : 1;
const f = new Map();
for (const l of cs.loads || []) if (l.type === 'node' && floorOf.has(l.node)) f.set(floorOf.get(l.node), (f.get(floorOf.get(l.node)) || 0) + Math.abs(l.F[idx]));
const drifts = storyDrifts(sol.prep, R);
let cum = 0, sw = 0, sf = 0;
const floors = [];
for (const d of drifts) { // ascending in z
cum += idx === 0 ? d.dx : d.dy;
const fl = c.floors.find((q) => q.id === d.id), w = (fl ? fl.w : 0) * 1000, ff = f.get(d.id) || 0, delta = cum / 1000; // N, N, m
floors.push({ id: d.id, w, f: ff, delta });
if (w > 0) { weights = true; sw += w * delta * delta; }
sf += ff * delta;
}
if (!(sw > 0) || !(sf > 0)) continue;
out[dir] = { T: 2 * Math.PI * Math.sqrt(sw / (G_ACC * sf)), floors };
out.ok = true;
}
if (!out.ok) out.error = weights ? 'no-motion' : 'no-weights';
return out;
}
Weight of each floor from the loads
64-bit
The vertical load of chosen load cases gathered at each floor, as its seismic weight.
node load = node forces + member equivalent loads + plate corner loads; each node's load goes to the floor at or above it
Vertical loads are accumulated in a Float64Array per node and then added per floor in node order. A node within 1 mm of the base gives its load to the ground, and a floor takes loads from up to 1 mm above its height; both limits are absolute.
frame/frame-concrete.js
lines 200 to 234
CadCraft 0.8.2
/** The weight of each floor from cases (see the header): downward N per floor, what went to the ground, and the total applied. */
export function floorWeights(model, entries, floors) {
const prep = prepare(model);
if (!prep || prep.error) return { ok: false, error: prep?.error?.code || 'no-solution' };
const list = entries.map(({ id, f }) => ({ c: model.cases.find((q) => q.id === id), f })).filter((e) => e.c && e.f);
const { nodeF, memLoads } = loadSet(prep, list.map(({ c, f }) => ({ c: { ...c, loads: c.loads.filter((l) => l.type !== 'area') }, f }))); // area loads are the plates' (below)
const nn = prep.nn, Fz = new Float64Array(nn); // vertical load at each node, N, up positive
for (let k = 0; k < nn; k++) Fz[k] = nodeF[6 * k + 2];
prep.mem.forEach((e, k) => {
const p = new Float64Array(12);
for (const pz of memLoads[k]) equivalentLoads(pz, e.ax.L, p);
const { ex, ey, ez } = e.ax;
Fz[e.i] += ex[2] * p[0] + ey[2] * p[1] + ez[2] * p[2];
Fz[e.j] += ex[2] * p[6] + ey[2] * p[7] + ez[2] * p[8];
});
for (const p of prep.shell?.pl || []) {
for (const { c, f } of list) {
let q = c.selfWeight ? -c.selfWeight * f * p.rho * 1e-9 * G_ACC * p.t : 0; // N/mm² along z
for (const l of c.loads) if (l.type === 'area' && l.plate === p.id) q += l.dir === 'Z' ? l.p * f : l.dir === 'n' ? l.p * f * p.el.ax.ez[2] : 0;
if (q) p.nodes.forEach((nd, k) => { Fz[nd] += p.ca[k] * q; });
}
}
const fl = [...floors].sort((a, b) => a.z - b.z);
const base = Math.min(...model.supports.map((s) => prep.P[prep.idx.get(s.node)][2]));
const weights = Object.fromEntries(fl.map((f) => [f.id, 0]));
let ground = 0, total = 0;
for (let k = 0; k < nn; k++) {
const z = prep.P[k][2], w = -Fz[k];
total += w;
if (!fl.length || z <= base + 1) { ground += w; continue; }
const up = fl.find((f) => f.z >= z - 1) || fl[fl.length - 1];
weights[up.id] += w;
}
return { ok: true, weights, total, ground };
}
In detail
Method
ASCE/SEI 7-22 12.8.1 to 12.8.6 and Table 12.12-1; Rayleigh's quotient for the period of the analyzed frame. The file's header lists the clauses.
Number types
64-bit numbers. Forces N, lengths mm, weights entered in kN and multiplied by 1000.
Tolerances
Floor heights are grouped within 1 mm and floor levels suggested by the app are rounded to 0.1 mm. A load at or within 1 mm below a floor goes to that floor.
Limits
Equivalent lateral force only. Wind is not built. The period from analysis is capped at Cu × Ta.
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.