8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
274 lines
12 KiB
Python
274 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""Camber and deflections the way the reference tool tabulates them.
|
||
|
||
Reverse-engineered from RBA5 p.14 ("Camber and Deflections"), which prints
|
||
one row per load case per stage with a multiplier, then the total:
|
||
|
||
* **Multipliers are ACI 318-19 24.2.4.1's long-term factor, not PCI's.**
|
||
Each row's multiplier is 1 + ξ / (1 + 50·ρ') with ρ' = A's / (b·d) at
|
||
midspan. RBA5 (ρ' = 1.76 / (16 × 20) = 0.0055): ξ = 2.0 gives 2.57 and
|
||
ξ = 1.0 gives 1.78 -- exactly the printed 2.57 (SW, Dc, sustained live,
|
||
final) and 1.78 (SW at start of construction; topping, final). The
|
||
settings page's "Self Weight 1 / 1.3 / 2" and "Add'l Dead - / 0 / 2" are
|
||
therefore ξ per stage (erection / construction / end), and live load
|
||
always carries 1.00.
|
||
|
||
* **Stiffness is the effective moment of inertia per station**, ACI 318-19
|
||
24.2.3.5 (Bischoff): Ie = Icr / (1 - (2/3·Mcr/Ma)²·(1 - Icr/Ig)) where
|
||
Ma > 2/3·Mcr, else Ig -- with Ig and Icr both TRANSFORMED (the tool's
|
||
"Transform Reinforcement: All"), Mcr = fr·Sb on the transformed section,
|
||
and Ma the service moment of the STAGE's total load at that station.
|
||
At the start of construction only self weight is on, nothing cracks, and
|
||
SW deflects on the uncracked section with Eci (printed 0.029 / 1.78 =
|
||
0.0163" -- ours 0.0160"). At the final stage the same self weight
|
||
deflects on the cracked stiffness of the FULL service load (0.113 / 2.57
|
||
= 0.044").
|
||
|
||
* Self weight uses Eci when the setting says so; every other case Ec.
|
||
* Live load is patterned; the reported live deflection is the worst
|
||
pattern's, and the total takes the worst pattern too.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
from typing import Dict, List, Optional, Sequence, Tuple
|
||
|
||
from . import loads as ld, section as sec_mod, statics
|
||
from .model import Beam
|
||
|
||
|
||
def cracked_inertia(beam: Beam, x_in: float, positive: bool = True,
|
||
ec_ksi: Optional[float] = None) -> Tuple[float, float, float]:
|
||
"""(Icr, kd, A_cr) of the cracked transformed section at x, elastic
|
||
(n·As tension, (n-1)·A's compression), solving the NA depth kd from
|
||
first moments. Positive bending: compression at the top."""
|
||
mats = beam.materials
|
||
ec = ec_ksi or mats.Ec
|
||
n_bar = mats.rebar.es_ksi / ec
|
||
n_str = mats.strand.ep_ksi / ec
|
||
sec = beam.section
|
||
h = sec.height_in
|
||
layers = [] # (depth from compression face, area, n)
|
||
for bx, by, area, db, e0, e1, row in beam.bars_at(x_in):
|
||
depth = (h - by) if positive else by
|
||
layers.append((depth, area, n_bar))
|
||
for g in beam.strands:
|
||
for sx, sy, area in g.strands(sec):
|
||
depth = (h - sy) if positive else sy
|
||
layers.append((depth, area, n_str))
|
||
if not any(d > 0 for d, a, nn in layers):
|
||
return sec.ixx, h, sec.area
|
||
block = sec.compression_area if positive else sec.compression_area_from_bottom
|
||
|
||
def first_moment(kd):
|
||
a, ybar = block(kd)
|
||
q = a * (kd - ybar) # concrete about the NA (above it)
|
||
for depth, area, nn in layers:
|
||
eq = (nn - 1.0) * area if depth < kd else nn * area
|
||
q += eq * (kd - depth) # positive above the NA
|
||
return q
|
||
|
||
lo, hi = 1e-4, h
|
||
for _ in range(100):
|
||
mid = (lo + hi) / 2.0
|
||
if first_moment(mid) > 0:
|
||
hi = mid
|
||
else:
|
||
lo = mid
|
||
kd = (lo + hi) / 2.0
|
||
a_c, ybar = block(kd)
|
||
# concrete: I about its own centroid + shift to the NA
|
||
icr = 0.0
|
||
top = h if positive else 0.0
|
||
for x0, y0, w, hh in sec.rects:
|
||
# part of this rect within the compression block
|
||
if positive:
|
||
lo_y = max(y0, h - kd)
|
||
hi_y = min(y0 + hh, h)
|
||
else:
|
||
lo_y = max(y0, 0.0)
|
||
hi_y = min(y0 + hh, kd)
|
||
if hi_y > lo_y:
|
||
d_h = hi_y - lo_y
|
||
centre = (lo_y + hi_y) / 2.0
|
||
depth_c = (h - centre) if positive else centre
|
||
icr += w * d_h ** 3 / 12.0 + w * d_h * (kd - depth_c) ** 2
|
||
a_cr = a_c
|
||
for depth, area, nn in layers:
|
||
eq = (nn - 1.0) * area if depth < kd else nn * area
|
||
icr += eq * (depth - kd) ** 2
|
||
a_cr += eq
|
||
return icr, kd, a_cr
|
||
|
||
|
||
def cracking_moment_kin(beam: Beam, x_in: float, positive: bool = True) -> float:
|
||
mats = beam.materials
|
||
fr = 7.5 * mats.lam * math.sqrt(mats.fc_ksi * 1000.0) / 1000.0
|
||
p = sec_mod.props_at(beam, x_in)
|
||
return fr * (p.sb if positive else p.st)
|
||
|
||
|
||
def effective_inertia(beam: Beam, x_in: float, ma_kin: float) -> float:
|
||
"""ACI 318-19 24.2.3.5 at one station for the service moment `ma`."""
|
||
positive = ma_kin >= 0
|
||
ig = sec_mod.props_at(beam, x_in).ixx
|
||
mcr = cracking_moment_kin(beam, x_in, positive)
|
||
ma = abs(ma_kin)
|
||
if ma <= (2.0 / 3.0) * mcr or mcr <= 0:
|
||
return ig
|
||
icr, kd, a_cr = cracked_inertia(beam, x_in, positive)
|
||
if icr >= ig:
|
||
return ig
|
||
denom = 1.0 - ((2.0 / 3.0) * mcr / ma) ** 2 * (1.0 - icr / ig)
|
||
return min(ig, icr / denom) if denom > 0 else ig
|
||
|
||
|
||
def long_term_multiplier(beam: Beam, xi: float) -> float:
|
||
"""1 + ξ / (1 + 50·ρ'), ρ' = compression steel ratio at midspan
|
||
(ACI 318-19 24.2.4.1.1 / 24.2.4.1.3)."""
|
||
if xi <= 0:
|
||
return 1.0
|
||
x_mid = beam.length_in / 2.0
|
||
h = beam.section.height_in
|
||
bars = beam.bars_at(x_mid)
|
||
# compression steel for positive bending = bars in the top half
|
||
as_c = sum(a for bx, by, a, db, e0, e1, row in bars if by > h / 2.0)
|
||
tens = [(a, h - by) for bx, by, a, db, e0, e1, row in bars if by <= h / 2.0]
|
||
d = (sum(a * dd for a, dd in tens) / sum(a for a, _ in tens)) if tens else 0.9 * h
|
||
b = beam.section.width_at(h - 1e-6) or beam.section.width_in
|
||
rho_c = as_c / (b * d) if b * d > 0 else 0.0
|
||
return 1.0 + xi / (1.0 + 50.0 * rho_c)
|
||
|
||
|
||
def _deflect(beam: Beam, loads, grid, ei) -> List[float]:
|
||
if not loads:
|
||
return [0.0] * len(grid)
|
||
xa, xb = beam.supports[0].x_in, beam.supports[1].x_in
|
||
res = statics.solve(loads, xa, xb, grid)
|
||
return statics.deflect(res, ei, xa, xb)
|
||
|
||
|
||
def analyse(beam: Beam, grid: Sequence[float]) -> dict:
|
||
s = beam.settings
|
||
mats = beam.materials
|
||
xa, xb = beam.supports[0].x_in, beam.supports[1].x_in
|
||
span = xb - xa
|
||
grid = list(grid)
|
||
n = len(grid)
|
||
e_sw = mats.Eci if s.ec_for_sw_defl.lower() == 'eci' else mats.Ec
|
||
ec = mats.Ec
|
||
|
||
sw = ld.self_weight(beam)
|
||
dnc = ld.case_loads(beam, 'deadnc')
|
||
dt = ld.case_loads(beam, 'topping')
|
||
dc = ld.case_loads(beam, 'deadc')
|
||
sus = ld.case_loads(beam, 'sus_live')
|
||
roof = ld.case_loads(beam, 'roof')
|
||
live_full = ld.case_loads(beam, 'live')
|
||
|
||
# multipliers (ξ per stage from the settings)
|
||
m_sw_start = long_term_multiplier(beam, s.sw_mult.get('erection', 1.0))
|
||
m_sw_final = long_term_multiplier(beam, s.sw_mult.get('end', 2.0))
|
||
m_dead_final = long_term_multiplier(beam, s.addl_dead_mult.get('end', 2.0))
|
||
m_top_final = long_term_multiplier(beam, s.sw_mult.get('erection', 1.0))
|
||
m_sus_final = m_dead_final
|
||
|
||
# --- start of construction: self weight (+ Dnc + Dt) on the uncracked
|
||
# section, judged for cracking under that load alone
|
||
start_loads = sw + dnc + dt
|
||
ma_start = statics.solve(start_loads, xa, xb, grid).moment
|
||
ie_start = [effective_inertia(beam, x, ma_start[i]) for i, x in enumerate(grid)]
|
||
d_sw_start = _deflect(beam, sw, grid, [e_sw * ie for ie in ie_start])
|
||
d_dnc_start = _deflect(beam, dnc, grid, [ec * ie for ie in ie_start])
|
||
d_dt_start = _deflect(beam, dt, grid, [ec * ie for ie in ie_start])
|
||
start_total = [m_sw_start * d_sw_start[i] + d_dnc_start[i] + d_dt_start[i]
|
||
for i in range(n)]
|
||
|
||
# --- final: worst live pattern for the total and for the live row
|
||
best = None
|
||
for pat in ld.patterns(beam):
|
||
live = ld.live_loads_for(beam, pat, 'live')
|
||
roof_p = ld.live_loads_for(beam, pat, 'roof')
|
||
# Two stiffnesses: the SUSTAINED rows (self weight, dead, topping,
|
||
# sustained live) deflect on Ie judged under the sustained load
|
||
# alone; the transient rows (live, roof) on Ie under the full
|
||
# service load. Measured on RBA5: with one Ie for everything the
|
||
# dead rows came out 5 % too soft (0.519 vs 0.495) while the live
|
||
# row was exact; Ie from the dead-only moment closes that gap.
|
||
sust_loads = sw + dnc + dt + dc + sus
|
||
final_loads = sust_loads + live + roof_p
|
||
ma_s = statics.solve(sust_loads, xa, xb, grid).moment
|
||
ma = statics.solve(final_loads, xa, xb, grid).moment
|
||
ie_s = [effective_inertia(beam, x, ma_s[i]) for i, x in enumerate(grid)]
|
||
ie = [effective_inertia(beam, x, ma[i]) for i, x in enumerate(grid)]
|
||
ei_sw = [e_sw * v for v in ie_s]
|
||
ei_s = [ec * v for v in ie_s]
|
||
ei_c = [ec * v for v in ie]
|
||
rows = {
|
||
'SW': (m_sw_final, _deflect(beam, sw, grid, ei_sw)),
|
||
'Dnc': (m_dead_final, _deflect(beam, dnc, grid, ei_s)),
|
||
'Dt': (m_top_final, _deflect(beam, dt, grid, ei_s)),
|
||
'Dc': (m_dead_final, _deflect(beam, dc, grid, ei_s)),
|
||
'Live': (1.0, _deflect(beam, live, grid, ei_c)),
|
||
'Sust. Live': (m_sus_final, _deflect(beam, sus, grid, ei_s)),
|
||
'Roof': (1.0, _deflect(beam, roof_p, grid, ei_c)),
|
||
}
|
||
total = [sum(m * d[i] for m, d in rows.values()) for i in range(n)]
|
||
sustained = [sum(m * d[i] for k, (m, d) in rows.items()
|
||
if k not in ('Live', 'Roof')) for i in range(n)]
|
||
live_row = [rows['Live'][1][i] + rows['Roof'][1][i] for i in range(n)]
|
||
key = max(abs(t) for t in total)
|
||
if best is None or key > best[0]:
|
||
best = (key, rows, total, sustained, live_row, ie, pat)
|
||
_, rows, total, sustained, live_row, ie, pat = best
|
||
|
||
idx = list(range(n)) # the tool reports over the whole length
|
||
|
||
# --- "prior / after installation of non-structural elements" split.
|
||
# RBA1/2/3/7 print the final stage in two blocks -- SW + Dnc (with their
|
||
# long-term multipliers) as "prior", Dt + Dc + live + sustained live +
|
||
# roof as "after" -- and judge the AFTER block against the total-load
|
||
# limit (RBA1: table total 0.423", reported "Defl. Total" 0.023" = the
|
||
# after block, limit L/480 -> PASS). RBA5 (composite, no Dnc load) and
|
||
# RBA6 ("Precast Only", Dnc present) print one block and judge the
|
||
# whole total. Inferred rule from those six packages: the split is
|
||
# made when the section behaves compositely AND a non-composite dead
|
||
# load exists. ACI 318-19 Table 24.2.2's "part of the total deflection
|
||
# occurring after attachment of nonstructural elements" is the idea.
|
||
composite = (s.composite_shear or '').lower() not in ('precast_only', 'precast only', 'none')
|
||
split = composite and any(abs(v) > 1e-12 for v in rows['Dnc'][1])
|
||
prior = [rows['SW'][0] * rows['SW'][1][i] + rows['Dnc'][0] * rows['Dnc'][1][i]
|
||
for i in range(n)]
|
||
after = [total[i] - prior[i] for i in range(n)]
|
||
judged = after if split else total
|
||
t_down = max(judged[i] for i in idx)
|
||
t_up = min(judged[i] for i in idx)
|
||
l_down = max(live_row[i] for i in idx)
|
||
l_up = min(live_row[i] for i in idx)
|
||
lim_t = span / s.total_defl_ratio
|
||
lim_l = span / s.live_defl_ratio
|
||
cracked = sum(1 for i in idx if ie[i] < sec_mod.props_at(beam, grid[i]).ixx - 1e-6)
|
||
return {
|
||
'span_in': span,
|
||
'total_down': t_down, 'total_up': -t_up, 'total_limit': lim_t,
|
||
'live_down': l_down, 'live_up': -l_up, 'live_limit': lim_l,
|
||
'total_util': max(t_down, -t_up) / lim_t if lim_t else 0.0,
|
||
'live_util': max(l_down, -l_up) / lim_l if lim_l else 0.0,
|
||
'total_ok': max(t_down, -t_up) <= lim_t + 1e-9,
|
||
'live_ok': max(l_down, -l_up) <= lim_l + 1e-9,
|
||
'rows': {k: {'multiplier': m, 'elastic': d} for k, (m, d) in rows.items()},
|
||
'total': total, 'sustained': sustained, 'live': live_row,
|
||
'prior': prior, 'after': after, 'split': split, 'judged': judged,
|
||
'table_total_down': max(total), 'table_total_up': -min(total),
|
||
'start_total': start_total, 'start_sw_multiplier': m_sw_start,
|
||
'ie': ie, 'pct_cracked': 100.0 * cracked / n if n else 0.0,
|
||
'multipliers': {'self_weight': m_sw_final, 'addl_dead': m_dead_final,
|
||
'topping': m_top_final, 'live': 1.0},
|
||
'e_sw': e_sw, 'e': ec,
|
||
'i': sec_mod.props_at(beam, (xa + xb) / 2.0).ixx,
|
||
'icr_mid': cracked_inertia(beam, (xa + xb) / 2.0)[0],
|
||
'pattern': pat,
|
||
'max_start': min(start_total) if start_total else 0.0,
|
||
'max_sustained': min(sustained) if sustained else 0.0,
|
||
}
|