Reference modules
Three modules ship in the app's store. They are the examples to copy from: each is Verified, each types no engineering number a capability can give, and between them they use every capability and most of the UI.
| Module | Computes | Capabilities | Download |
|---|---|---|---|
| DSM compression | Pne, Pnl, Pnd and Pn by the Direct Strength Method | getModel firstYield stress signature modes | .ckext |
| Rounded C / Z section | A C or Z with rounded corners, its properties, offered to the analysis | sectionProps proposeModel | .ckext |
| Thickness study | Local and distortional buckling against sheet thickness | getModel firstYield stress signature modes proposeModel | .ckext |
Install them from Extensions › Store, and read their code with View code, or fork one into the builder. The sources below are copies of version 1.0.0 of each, taken when this guide was written; the installed module's View code is always the current one. Each source folder also has a README with the worked examples' full working.
DSM compression#
The Direct Strength Method column strength, Pn = min(Pne, Pnl, Pnd), from the section's signature curve and first yield, for AISI S100-16 or AS/NZS 4600:2018 (the equations are the same; the clause numbers in the calc lines differ).
How it works#
firstYieldgives Py, so the area is A = Py / fy: the engine's area, not one computed in Python.stresswith P = Py gives the reference stress of uniform compression at the squash load. Those stresses go on a copy of the model's nodes, andsignaturesolves the copy, so every load factor on the curve is Pcr / Py, whatever loads the app has.modesat each minimum's length gives its cFSM split. Local is the shortest-wavelength minimum and distortional the next, skipping minima cFSM calls mostly global (G above 50 %). The user can pick other minima, or type Pcrl / Py and Pcrd / Py in.- Global: Le = 0 is continuously braced (Pne = Py); a given Fcre gives Pcre = A Fcre; otherwise the curve's load factor at L = Le.
- The DSM equations, each with its clause in a calc line, and φc Pn with φc = 0.85.
Patterns worth copying#
- Default lengths like CUFSM's:
lengths_foruses the analysis's lengths, or CUFSM's own default of 50 lengths from a tenth of the section's size to a thousand times it, and adds Le so the curve has a point there. - A curve for
build_ui: the module-levelLASTcarries the curve and its classified minima fromcheckto abuckling.signaturenode, with each minimum'skindlabelling its marker. - Calc lines with clauses: the
line()helper fillsstandard,yearandclausefrom the chosen code, so switching code switches every reference. - Running only what is needed: the signature curve is solved only when a value is read off it.
- Inputs that fall back: blank fy means the analysis's, and 0 means "from the curve".
- A row instead of an exception when the curve has no distortional minimum:
no distortional minimum: enter Pcrd/Py.
Its worked examples#
Three examples from the AISI Direct Strength Method Design Guide (CF06-1, 2006), on the guide's own CUFSM models converted to mm and MPa: Example 8.1-4 (the 9CS2.5x059 C, braced), 8.1-5 (the same C with Fe given) and 8.5-3 (the 8ZS2.25x059 Z, braced, with Pcrl and Pcrd read off the engine's curve, whose minima match the guide's file to 1e-6). The C has no distortional minimum in its first mode, so its examples take the guide's printed Pcr ratios as inputs. The README works every expected value from the guide's equations to more digits than the guide prints.
# DSM compression: the Direct Strength Method column strength Pn = min(Pne, Pnl, Pnd) from the
# engine's signature curve and first yield. Every number comes from a capability or an input;
# this file holds only the DSM equations, each cited where it is used.
#
# Units are the app's: N, mm, MPa. Forces are reported in kN.
import json, math, civilkit
CODES = {
"AISI S100-16": {
"std": "AISI S100", "year": "2016", "P": "P",
"global": "§E2", "local": "§E3", "dist": "§E4",
"old": "S100-04 App. 1 Eq. 1.2.1-",
},
"AS/NZS 4600:2018": {
"std": "AS/NZS 4600", "year": "2018", "P": "N",
"global": "7.2.1.2", "local": "7.2.1.3", "dist": "7.2.1.4",
"old": None,
},
}
KINDS = ["G", "D", "L", "O"]
LAST = {"curve": [], "minima": []} # the last run's curve, drawn by build_ui
def num(inputs, key, default):
v = inputs.get(key)
if v is None or v == "":
return default
try:
return float(v)
except (TypeError, ValueError):
raise ValueError("%s must be a number, not %r" % (key, v))
def sig(v):
# five significant figures, for display; the 0.1 % worked-example tolerance is far coarser
return float("%.5g" % v) if v else v
def kn(v):
return sig(v / 1000.0)
def call(name, arg=None):
f = getattr(civilkit, name)
return json.loads(f(json.dumps(arg)) if arg is not None else f())
def logspace(a, b, n):
la, lb = math.log10(a), math.log10(b)
return [10 ** (la + (lb - la) * k / (n - 1)) for k in range(n)]
def lengths_for(model, le):
ls = list((model.get("analysis") or {}).get("lengths") or [])
if len(ls) < 2:
# CUFSM's own default (templatecalc.m): 50 lengths from big/10 to 1000 big, big = the
# section's largest dimension
xs = [n["x"] for n in model["nodes"]]
zs = [n["z"] for n in model["nodes"]]
big = max(max(xs) - min(xs), max(zs) - min(zs))
ls = logspace(big / 10, big * 1000, 50)
if le > 0:
ls.append(le)
return sorted(set(ls))
def pick(minima, k, what):
if k < 1:
return None
k = int(k)
if k > len(minima):
raise ValueError("%s minimum %d asked for, but the curve has %d minima" % (what, k, len(minima)))
return minima[k - 1]
def check(inputs):
code = inputs.get("code") or "AISI S100-16"
C = CODES.get(code) or CODES["AISI S100-16"]
P = C["P"]
def line(clause, label, eq, old=None):
if C["old"] and old:
clause = "%s (%s%s)" % (clause, C["old"], old)
return {"standard": C["std"], "year": C["year"], "clause": clause, "label": label, "eq": eq}
model = call("getModel")
if not model.get("nodes"):
raise ValueError("There is no model to check.")
fy = num(inputs, "fy", model.get("fy") or 0)
if not fy > 0:
raise ValueError("Enter fy (MPa): the model has none.")
le = num(inputs, "Le", 0)
fcre = num(inputs, "Fcre", 0)
rl_in = num(inputs, "pcrl_py", 0)
rd_in = num(inputs, "pcrd_py", 0)
y = call("firstYield", {"fy": fy})
Py = y["Py"]
A = Py / fy
lines = [line("", "Squash load (first yield, from the engine)",
"%sy = A fy = %.6g mm² × %.6g MPa = %.6g kN" % (P, A, fy, Py / 1000))]
rows = [{"label": "Py", "value": kn(Py), "unit": "kN"}]
# The signature curve, with the reference stress uniform compression at Py, so a load factor
# is Pcr / Py. Run only when something is read off it.
need = rl_in <= 0 or rd_in <= 0 or (fcre <= 0 and le > 0)
minima, curve, local, dist = [], [], None, None
if need:
st = call("stress", {"P": Py})["stress"]
ref = dict(model)
ref["nodes"] = [dict(n, stress=s) for n, s in zip(model["nodes"], st)]
s = call("signature", {"model": ref, "bc": "S-S", "terms": 1, "lengths": lengths_for(model, le if fcre <= 0 else 0)})
curve = s["curve"]
for mn in s["minima"]:
md = call("modes", {"model": ref, "bc": "S-S", "terms": 1, "length": mn["length"], "n": 1})["modes"][0]
mn["cls"] = md["cls"]
mn["kind"] = KINDS[md["cls"].index(max(md["cls"]))]
minima.append(mn)
# Local is the shortest-wavelength minimum and distortional the next, skipping minima
# cFSM calls mostly global (G above 50 %); or the minima the user picked.
cand = [mn for mn in minima if mn["cls"][0] < 50]
local = pick(minima, num(inputs, "local_pick", 0), "The local") or (cand[0] if cand else None)
dist = pick(minima, num(inputs, "dist_pick", 0), "The distortional") or (cand[1] if len(cand) > 1 else None)
lines.append(line("", "Signature curve (S-S, one term, reference stress fy: λ = Pcr / %sy)" % P,
"%d lengths, %d minima: %s" % (len(curve), len(minima), "; ".join(
"L = %.5g mm, λ = %.5g (cFSM G %.1f %%, D %.1f %%, L %.1f %%, O %.1f %%)" % (
mn["length"], mn["lf"], mn["cls"][0], mn["cls"][1], mn["cls"][2], mn["cls"][3])
for mn in minima))))
LAST["curve"] = curve
LAST["minima"] = [{"index": mn["index"], "length": mn["length"], "lf": mn["lf"], "kind": mn["kind"]} for mn in minima]
# Global: braced, a given Fcre, or the curve at L = Le
if fcre > 0:
Pcre = A * fcre
lines.append(line(C["global"], "Global buckling load (Fcre given)", "%scre = A Fcre = %.6g × %.6g = %.6g kN" % (P, A, fcre, Pcre / 1000)))
elif le > 0:
lf = [p[1] for p in curve if abs(p[0] - le) <= 1e-9 * le][0]
Pcre = lf * Py
lines.append(line(C["global"], "Global buckling load from the curve at L = Le (valid where the curve there is global)",
"%scre = λ(%.6g mm) %sy = %.6g × %.6g = %.6g kN" % (P, le, P, lf, Py / 1000, Pcre / 1000)))
else:
Pcre = None
if Pcre is None:
Pne = Py
lines.append(line(C["global"], "Continuously braced (Le = 0): global buckling is prevented", "%sne = %sy = %.6g kN" % (P, P, Pne / 1000)))
else:
lc = math.sqrt(Py / Pcre)
if lc <= 1.5:
Pne = 0.658 ** (lc * lc) * Py
eq = "λc = √(%sy/%scre) = %.4f ≤ 1.5: %sne = 0.658^(λc²) %sy = %.6g kN" % (P, P, lc, P, P, Pne / 1000)
else:
Pne = 0.877 / (lc * lc) * Py
eq = "λc = √(%sy/%scre) = %.4f > 1.5: %sne = (0.877/λc²) %sy = %.6g kN" % (P, P, lc, P, P, Pne / 1000)
lines.append(line(C["global"], "Global (flexural, torsional or flexural-torsional) strength", eq, "1 to 3"))
rows.append({"label": "Pcre", "value": kn(Pcre), "unit": "kN"})
rows.append({"label": "Pne", "value": kn(Pne), "unit": "kN"})
# Local, interacting with global
if rl_in > 0:
rl, src = rl_in, "given"
elif local:
rl, src = local["lf"], "the curve's minimum at L = %.5g mm" % local["length"]
rows.append({"label": "Lcrl", "value": sig(local["length"]), "unit": "mm"})
else:
rl, src = None, None
Pnl = None
if rl:
Pcrl = rl * Py
ll = math.sqrt(Pne / Pcrl)
if ll <= 0.776:
Pnl = Pne
eq = "λl = √(%sne/%scrl) = %.4f ≤ 0.776: %snl = %sne = %.6g kN" % (P, P, ll, P, P, Pnl / 1000)
else:
r = (Pcrl / Pne) ** 0.4
Pnl = (1 - 0.15 * r) * r * Pne
eq = "λl = √(%sne/%scrl) = %.4f > 0.776: %snl = [1 − 0.15 (%scrl/%sne)^0.4] (%scrl/%sne)^0.4 %sne = %.6g kN" % (
P, P, ll, P, P, P, P, P, P, Pnl / 1000)
lines.append(line(C["local"], "Local buckling: %scrl/%sy = %.5g (%s), %scrl = %.6g kN" % (P, P, rl, src, P, Pcrl / 1000), eq, "5 to 7"))
rows += [{"label": "Pcrl/Py", "value": sig(rl)}, {"label": "Pcrl", "value": kn(Pcrl), "unit": "kN"},
{"label": "Pnl", "value": kn(Pnl), "unit": "kN"}]
else:
rows.append({"label": "Pnl", "value": "no local minimum: enter Pcrl/Py", "status": "warn"})
# Distortional, with yield (not with global)
if rd_in > 0:
rd, src = rd_in, "given"
elif dist:
rd, src = dist["lf"], "the curve's minimum at L = %.5g mm" % dist["length"]
rows.append({"label": "Lcrd", "value": sig(dist["length"]), "unit": "mm"})
else:
rd, src = None, None
Pnd = None
if rd:
Pcrd = rd * Py
ld = math.sqrt(Py / Pcrd)
if ld <= 0.561:
Pnd = Py
eq = "λd = √(%sy/%scrd) = %.4f ≤ 0.561: %snd = %sy = %.6g kN" % (P, P, ld, P, P, Pnd / 1000)
else:
r = (Pcrd / Py) ** 0.6
Pnd = (1 - 0.25 * r) * r * Py
eq = "λd = √(%sy/%scrd) = %.4f > 0.561: %snd = [1 − 0.25 (%scrd/%sy)^0.6] (%scrd/%sy)^0.6 %sy = %.6g kN" % (
P, P, ld, P, P, P, P, P, P, Pnd / 1000)
lines.append(line(C["dist"], "Distortional buckling: %scrd/%sy = %.5g (%s), %scrd = %.6g kN" % (P, P, rd, src, P, Pcrd / 1000), eq, "8 to 10"))
rows += [{"label": "Pcrd/Py", "value": sig(rd)}, {"label": "Pcrd", "value": kn(Pcrd), "unit": "kN"},
{"label": "Pnd", "value": kn(Pnd), "unit": "kN"}]
else:
rows.append({"label": "Pnd", "value": "no distortional minimum: enter Pcrd/Py", "status": "warn"})
if Pnl is not None and Pnd is not None:
Pn = min(Pne, Pnl, Pnd)
gov = "global" if Pn == Pne and Pne < Py else "yield" if Pn == Py else "local" if Pn == Pnl else "distortional"
lines.append(line("", "Nominal strength", "%sn = min(%sne, %snl, %snd) = %.6g kN (%s governs)" % (P, P, P, P, Pn / 1000, gov)))
lines.append(line("", "Design strength: φc = 0.85 for a pre-qualified column (DSM Design Guide 8.1-4; check the section qualifies)",
"φc %sn = 0.85 × %.6g = %.6g kN" % (P, Pn / 1000, 0.85 * Pn / 1000)))
rows += [{"label": "Pn", "value": kn(Pn), "unit": "kN", "status": "ok"},
{"label": "φcPn", "value": kn(0.85 * Pn), "unit": "kN"},
{"label": "Governs", "value": gov}]
return {"result": rows, "calcLines": lines}
def build_ui(inputs, result, calcLines):
g = inputs.get
kids = [
{"type": "text", "value": "Direct Strength Method column strength from this section's signature curve. "
"Units: N, mm, MPa; forces in kN. Leave a buckling input at 0 to read it off the curve."},
{"type": "section", "title": "Member", "children": [
{"type": "field", "id": "code", "label": "Code", "inputType": "enum",
"options": list(CODES.keys()), "default": g("code", "AISI S100-16")},
{"type": "field", "id": "fy", "label": "fy (MPa), blank: the analysis's", "inputType": "number", "default": g("fy", "")},
{"type": "field", "id": "Le", "label": "Le (mm), 0: continuously braced", "inputType": "number", "default": g("Le", 0)},
]},
{"type": "section", "title": "Elastic buckling (0: from the signature curve)", "collapsed": True, "children": [
{"type": "field", "id": "Fcre", "label": "Fcre (MPa), e.g. a closed-form strong-axis Fe", "inputType": "number", "default": g("Fcre", 0)},
{"type": "field", "id": "pcrl_py", "label": "Pcrl / Py", "inputType": "number", "default": g("pcrl_py", 0)},
{"type": "field", "id": "pcrd_py", "label": "Pcrd / Py", "inputType": "number", "default": g("pcrd_py", 0)},
{"type": "field", "id": "local_pick", "label": "Local: minimum no. (0: the first)", "inputType": "integer", "default": g("local_pick", 0)},
{"type": "field", "id": "dist_pick", "label": "Distortional: minimum no. (0: the next)", "inputType": "integer", "default": g("dist_pick", 0)},
]},
{"type": "button", "id": "run", "label": "Run", "action": "run-check"},
]
if result:
kids.append({"type": "result", "items": result})
if LAST["curve"]:
kids.append({"type": "buckling.signature", "title": "Signature curve",
"curve": LAST["curve"], "minima": LAST["minima"], "xlabel": "half-wavelength (mm)", "ylabel": "Pcr / Py"})
if calcLines:
kids.append({"type": "calc", "lines": calcLines})
return {"type": "panel", "title": "DSM compression", "children": kids}
Rounded C / Z section#
Builds a lipped or plain C or Z with one radius at all four corners, node by node, the way CUFSM's template does
(templatecalc.m, and template_out_to_in.m for outside dimensions with an inside radius). It
shows the section's properties from the engine and offers the section to the analysis.
How it works#
buildturns the inputs into a model: the template's corner points, the flats and arcs meshed with the given number of elements, one material.sectionPropswith{"model": m}gives the properties of that model, not of the analysis's. A product of inertia below 1e-9 of the larger I is shown as 0 (a symmetric C's comes back as rounding noise).- While Offer it to the analysis is ticked, every run calls
proposeModel; the app shows the proposal banner, and the user previews, uses or dismisses it.
Patterns worth copying#
- Geometry is the module's; properties are the engine's. The module never computes A or I itself.
- A choice made with a field, not a button:
checkcannot tell which button was pressed, so the proposal is controlled by a boolean field, and the run button is labelled Use this section. - Input checks with sentences:
Give both lips, or neither, raised asValueError. - A compact form:
rows of short fields, anenumwith value/label options, and the mesh and material in acollapsedsection. - Drawing what it built: a
buckling.sectionnode with the generatedmodel.
Its worked examples#
Two cases of CUFSM's own template, run in Octave by the cufsm-rs test oracle, with the properties that run recorded: a
rounded lipped C (centreline dimensions; 25 nodes, A, Ixx, Izz) and an outside-dimension lipped Z (29 nodes, A, Ixx,
Izz, Ixz, I11, I22). The engine's properties of the oracle's own node lists agree with the recorded ones to 1e-15, so
the examples test this module's geometry and nothing else. Its examples have no model: the module never
reads one.
# A lipped (or plain) C or Z section with rounded corners, built node by node the way CUFSM's
# template does (helpers/templatecalc.m, with template_out_to_in.m for outside dimensions), its
# properties from the engine, and proposed to the analysis for the user to accept.
#
# Units: N, mm, MPa. The geometry is this file's; every property comes from sectionProps.
import json, math, civilkit
LAST = {"model": None}
def num(inputs, key, default):
v = inputs.get(key)
if v is None or v == "":
return default
try:
return float(v)
except (TypeError, ValueError):
raise ValueError("%s must be a number, not %r" % (key, v))
def sig(v):
return float("%.6g" % v) if v else v
def out_to_in(H, B1, D1, B2, D2, ri, t):
# template_out_to_in.m for 90 degree lips: outside dimensions and inside radius to the
# centreline flats and centreline radius the template takes (tan(q/2) = 1)
r = ri + t / 2 if ri != 0 else 0
h = H - t / 2 - r - r - t / 2
if D1 == 0:
b1, d1 = B1 - r - t / 2, 0
else:
b1, d1 = B1 - r - t / 2 - (r + t / 2), D1 - (r + t / 2)
if D2 == 0:
b2, d2 = B2 - r - t / 2, 0
else:
b2, d2 = B2 - r - t / 2 - (r + t / 2), D2 - (r + t / 2)
return h, b1, d1, b2, d2, r
def template(cz, h, b1, b2, d1, d2, r, nh, nb, nd, nr):
# templatecalc.m, q1 = q2 = 90 degrees and one radius for all four corners. Returns the node
# coordinates in order around the section, starting at the tip of lip 1.
q = math.pi / 2
lipped = not (d1 == 0 and d2 == 0)
if r == 0:
if not lipped:
geom = [(b1, 0), (0, 0), (0, h), (cz * b2, h)]
n = [nb, nh, nb]
else:
geom = [(b1 + d1 * math.cos(q), d1 * math.sin(q)), (b1, 0), (0, 0), (0, h), (cz * b2, h),
(cz * (b2 + d2 * math.cos(q)), h - d2 * math.sin(q))]
n = [nd, nb, nh, nb, nd]
else:
if not lipped:
geom = [(r + b1, 0), (r, 0), (0, r), (0, r + h), (cz * r, r + h + r), (cz * (r + b2), r + h + r)]
n = [nb, nr, nh, nr, nb]
else:
geom = [(r + b1 + r * math.cos(math.pi / 2 - q) + d1 * math.cos(q), r - r * math.sin(math.pi / 2 - q) + d1 * math.sin(q)),
(r + b1 + r * math.cos(math.pi / 2 - q), r - r * math.sin(math.pi / 2 - q)),
(r + b1, 0), (r, 0), (0, r), (0, r + h), (cz * r, r + h + r), (cz * (r + b2), r + h + r),
(cz * (r + b2 + r * math.cos(math.pi / 2 - q)), r + h + r - r + r * math.sin(math.pi / 2 - q)),
(cz * (r + b2 + r * math.cos(math.pi / 2 - q) + d2 * math.cos(q)), r + h + r - r + r * math.sin(math.pi / 2 - q) - d2 * math.sin(q))]
n = [nd, nr, nb, nr, nh, nr, nb, nr, nd]
straight = [1, 3, 5] if (r != 0 and not lipped) else [1, 3, 5, 7, 9]
pts = []
for i in range(1, len(geom)):
(x0, z0), (x1, z1) = geom[i - 1], geom[i]
pts.append((x0, z0))
k = n[i - 1]
if r == 0 or i in straight:
for j in range(1, k):
pts.append((x0 + (x1 - x0) * j / k, z0 + (z1 - z0) * j / k))
continue
for j in range(1, k): # a corner: points on the arc
if not lipped:
if i == 2:
xc, zc, q0, dq = r, r, math.pi / 2, math.pi / 2 * j / k
else: # i == 4
xc, zc, q0, dq = cz * r, r + h, (math.pi if cz == 1 else 0), cz * math.pi / 2 * j / k
elif i == 2:
xc, zc, q0, dq = r + b1, r, math.pi / 2 - q, q * j / k
elif i == 4:
xc, zc, q0, dq = r, r, math.pi / 2, math.pi / 2 * j / k
elif i == 6:
xc, zc, q0, dq = cz * r, r + h, (math.pi if cz == 1 else 0), cz * math.pi / 2 * j / k
else: # i == 8
xc, zc, q0, dq = cz * (r + b2), r + h + r - r, 3 * math.pi / 2, cz * q * j / k
pts.append((xc + r * math.cos(q0 + dq), zc - r * math.sin(q0 + dq)))
pts.append(geom[-1])
return pts
def build(inputs):
g = lambda k, d: num(inputs, k, d)
shape = inputs.get("shape") or "C"
dims = inputs.get("dims") or "centreline"
h, b1, b2, d1, d2, r, t = g("h", 200), g("b1", 75), g("b2", 75), g("d1", 20), g("d2", 20), g("r", 3), g("t", 1.5)
nh, nb, nd, nr = int(g("nh", 6)), int(g("nb", 3)), int(g("nd", 2)), int(g("nr", 2))
E, nu = g("E", 203000), g("nu", 0.3)
if not (t > 0 and h > 0 and b1 > 0 and b2 > 0 and min(d1, d2, r) >= 0):
raise ValueError("h, b1, b2 and t must be above zero, and d1, d2 and r at least zero")
if min(nh, nb) < 1 or (min(d1, d2) > 0 and nd < 1) or (r > 0 and nr < 1):
raise ValueError("Each flat and corner needs at least one element")
if (d1 == 0) != (d2 == 0):
raise ValueError("Give both lips, or neither (the template makes a lipped or a plain section)")
if dims == "outside":
h, b1, d1, b2, d2, r = out_to_in(h, b1, d1, b2, d2, r, t)
if min(h, b1, b2) <= 0 or min(d1, d2) < 0:
raise ValueError("The outside dimensions are too small for this thickness and radius")
pts = template(-1 if shape == "Z" else 1, h, b1, b2, d1, d2, r, nh, nb, nd, nr)
g_ = E / (2 * (1 + nu))
return {
"mats": [{"id": 100, "ex": E, "ey": E, "vx": nu, "vy": nu, "g": g_}],
"nodes": [{"x": x, "z": z, "free": [1, 1, 1, 1], "stress": 1.0} for x, z in pts],
"elems": [{"i": k, "j": k + 1, "t": t, "mat": 100} for k in range(len(pts) - 1)],
"springs": [], "constraints": [],
}
def check(inputs):
m = build(inputs)
LAST["model"] = m
p = json.loads(civilkit.sectionProps(json.dumps({"model": m})))
# a symmetric C's product of inertia and principal angle come back as rounding noise: show 0
if abs(p["Ixz"]) < 1e-9 * max(abs(p["Ixx"]), abs(p["Izz"])):
p["Ixz"], p["thetap"] = 0.0, 0.0
size = max(max(abs(n["x"]) for n in m["nodes"]), max(abs(n["z"]) for n in m["nodes"]))
for k in ("xcg", "zcg"):
if abs(p[k]) < 1e-9 * size:
p[k] = 0.0
rows = [
{"label": "Nodes", "value": len(m["nodes"])},
{"label": "A", "value": sig(p["A"]), "unit": "mm²"},
{"label": "Ixx", "value": sig(p["Ixx"]), "unit": "mm⁴"},
{"label": "Izz", "value": sig(p["Izz"]), "unit": "mm⁴"},
{"label": "Ixz", "value": sig(p["Ixz"]), "unit": "mm⁴"},
{"label": "I11", "value": sig(p["I11"]), "unit": "mm⁴"},
{"label": "I22", "value": sig(p["I22"]), "unit": "mm⁴"},
{"label": "θp", "value": sig(p["thetap"]), "unit": "°"},
{"label": "xcg", "value": sig(p["xcg"]), "unit": "mm"},
{"label": "zcg", "value": sig(p["zcg"]), "unit": "mm"},
]
if inputs.get("propose", True) not in (False, "false", 0):
civilkit.proposeModel(json.dumps(m))
return {"result": rows, "calcLines": []}
def build_ui(inputs, result, calcLines):
g = inputs.get
def f(id, label, d, typ="number"):
return {"type": "field", "id": id, "label": label, "inputType": typ, "default": g(id, d)}
kids = [
{"type": "text", "value": "A C or Z section with rounded corners, meshed as CUFSM's template does. "
"Units: mm and MPa. Each run offers the section to the analysis: use it or dismiss it."},
{"type": "row", "children": [
{"type": "field", "id": "shape", "label": "Shape", "inputType": "enum", "options": ["C", "Z"], "default": g("shape", "C")},
{"type": "field", "id": "dims", "label": "Dimensions", "inputType": "enum",
"options": [{"value": "centreline", "label": "centreline flats, centreline radius"},
{"value": "outside", "label": "outside, inside radius"}], "default": g("dims", "centreline")},
]},
{"type": "row", "children": [f("h", "h (mm)", 200), f("t", "t (mm)", 1.5), f("r", "r (mm)", 3)]},
{"type": "row", "children": [f("b1", "b1 (mm)", 75), f("b2", "b2 (mm)", 75)]},
{"type": "row", "children": [f("d1", "d1 (mm), 0: no lips", 20), f("d2", "d2 (mm)", 20)]},
{"type": "section", "title": "Mesh and material", "collapsed": True, "children": [
{"type": "row", "children": [f("nh", "web elements", 6, "integer"), f("nb", "per flange", 3, "integer"),
f("nd", "per lip", 2, "integer"), f("nr", "per corner", 2, "integer")]},
{"type": "row", "children": [f("E", "E (MPa)", 203000), f("nu", "ν", 0.3)]},
]},
f("propose", "Offer it to the analysis", True, "boolean"),
{"type": "button", "id": "run", "label": "Use this section", "action": "run-check"},
]
if result:
kids.append({"type": "result", "items": result})
if LAST["model"]:
kids.append({"type": "buckling.section", "title": "The generated section", "model": LAST["model"]})
return {"type": "panel", "title": "Rounded C / Z section", "children": kids}
Thickness study#
A showcase: for each thickness in a range it scales the current section, solves it fully, and finds the thinnest thickness whose local and distortional buckling both reach a target min(Pcrl, Pcrd) / Py.
How it works#
at_thicknessscales every element's t by t / tref on a copy of the model.bucklingdoes, for that copy, what DSM compression does:firstYield,stressat Py,signature, andmodesat every minimum to pick local and distortional.- The results go into a log-scale chart against t (with the target and the chosen t annotated), the thinnest, middle and thickest signature curves overlaid on log axes, a table of every thickness, and the thinnest passing section drawn with its stresses at Py.
- With Offer the thinnest passing section ticked, that section is proposed to the analysis.
Patterns worth copying#
- Many solves in one check: each thickness is a full signature curve.
Stepsis limited to 2 to 15 so a run stays well inside the 20-second limit. - Tabs for Charts, Table and the section, so a long result stays short.
- Log charts with
legend,grid,hoverandannotations. - A section drawn stressed: the picked model with the stresses at its Py on its nodes, in a
buckling.sectionwith"stress": True. - Honest calc lines: where the curve has no distortional minimum, a line says so and what that means for the target.
Its worked example#
One hand calc at both ends of the range: the app's default lipped C has a centreline 350 mm long, so A = 350 t; at t = 0.75 mm A = 262.5 mm², and at t = 3 mm Py = 1050 × 450 = 472.5 kN. The buckling values are the engine's (cufsm-rs, regression-tested against CUFSM) and are deliberately not pinned: no published source gives them.
# Thickness study: how local and distortional buckling of the current section change with the sheet
# thickness, and the thinnest thickness that meets a target. A showcase of what a module can do:
# read the model, call the engine (first yield, stress, signature curve, cFSM mode classes), draw
# log-scale charts and the section, show tables and cited calc lines, and propose a section back to
# the analysis. Every number comes from a capability or an input; this file only orchestrates.
#
# Units are the app's: N, mm, MPa.
import json, math, civilkit
KINDS = ["G", "D", "L", "O"]
LAST = {"study": [], "curves": [], "pick": None, "model": None, "target": None}
def call(name, arg=None):
f = getattr(civilkit, name)
return json.loads(f(json.dumps(arg)) if arg is not None else f())
def num(inputs, key, default):
v = inputs.get(key)
if v is None or v == "":
return default
try:
return float(v)
except (TypeError, ValueError):
raise ValueError("%s must be a number, not %r" % (key, v))
def sig(v):
return float("%.4g" % v) if v else v
def logspace(a, b, n):
la, lb = math.log10(a), math.log10(b)
return [10 ** (la + (lb - la) * k / (n - 1)) for k in range(n)]
def at_thickness(model, t, tref):
# every element scaled by t / tref, so a section with one thickness gets exactly t
m = dict(model)
m["elems"] = [dict(e, t=e["t"] * t / tref) for e in model["elems"]]
return m
def buckling(model, fy, lengths):
# Py from the engine; reference stress uniform compression at Py, so a load factor is Pcr / Py
Py = call("firstYield", {"model": model, "fy": fy})["Py"]
st = call("stress", {"model": model, "P": Py})["stress"]
ref = dict(model)
ref["nodes"] = [dict(n, stress=s) for n, s in zip(model["nodes"], st)]
s = call("signature", {"model": ref, "bc": "S-S", "terms": 1, "lengths": lengths})
minima = []
for mn in s["minima"]:
md = call("modes", {"model": ref, "bc": "S-S", "terms": 1, "length": mn["length"], "n": 1})["modes"][0]
mn["cls"] = md["cls"]
mn["kind"] = KINDS[md["cls"].index(max(md["cls"]))]
minima.append(mn)
# local: the shortest-wavelength minimum cFSM does not call mostly global; distortional: the next
cand = [mn for mn in minima if mn["cls"][0] < 50]
local = cand[0] if cand else None
dist = cand[1] if len(cand) > 1 else None
return {"Py": Py, "A": Py / fy, "curve": s["curve"], "minima": minima, "local": local, "dist": dist, "ref": ref}
def check(inputs):
model = call("getModel")
if not model.get("nodes"):
raise ValueError("There is no model to study.")
fy = num(inputs, "fy", model.get("fy") or 0)
if not fy > 0:
raise ValueError("Enter fy (MPa): the model has none.")
tref = max(e["t"] for e in model["elems"])
t0, t1 = num(inputs, "t_min", round(tref * 0.5, 3)), num(inputs, "t_max", round(tref * 3, 3))
n = int(num(inputs, "steps", 7))
target = num(inputs, "target", 0.5)
if not (0 < t0 < t1):
raise ValueError("t min must be positive and below t max.")
if not 2 <= n <= 15:
raise ValueError("Steps must be 2 to 15 (each is a full signature curve).")
xs = [nd["x"] for nd in model["nodes"]]
zs = [nd["z"] for nd in model["nodes"]]
big = max(max(xs) - min(xs), max(zs) - min(zs))
lengths = logspace(big / 10, big * 100, 40)
ts = [t0 + (t1 - t0) * k / (n - 1) for k in range(n)]
study, curves = [], []
for t in ts:
b = buckling(at_thickness(model, t, tref), fy, lengths)
rl = b["local"]["lf"] if b["local"] else None
rd = b["dist"]["lf"] if b["dist"] else None
worst = min(v for v in (rl, rd) if v is not None) if (rl or rd) else None
study.append({"t": t, "A": b["A"], "Py": b["Py"], "rl": rl, "rd": rd, "worst": worst,
"Ll": b["local"]["length"] if b["local"] else None,
"Ld": b["dist"]["length"] if b["dist"] else None})
curves.append((t, b["curve"]))
ok = [r for r in study if r["worst"] is not None and r["worst"] >= target]
pick = ok[0] if ok else None
LAST.update(study=study, target=target, pick=pick,
curves=[c for i, c in enumerate(curves) if i in (0, n // 2, n - 1)])
lines = [
{"label": "Method", "eq": "For each t: Py = A fy from the engine's first yield; the reference stress is "
"uniform compression at Py, so each signature-curve load factor is Pcr / Py. "
"Local = the shortest-wavelength minimum cFSM does not class as mostly global; "
"distortional = the next. %d thicknesses, %d half-wavelengths each, S-S, one term." % (n, len(lengths))},
{"label": "Target", "eq": "the smaller of Pcrl/Py and Pcrd/Py at least %.4g. For reference, the Direct "
"Strength Method gives no local reduction when Pcrl/Py ≥ 1/0.776² = 1.661 and no "
"distortional reduction when Pcrd/Py ≥ 1/0.561² = 3.177" % target,
"standard": "AISI S100", "year": "2016", "clause": "§E3.2, §E4.2"},
]
no_d = [r["t"] for r in study if r["rd"] is None]
if no_d:
lines.append({"label": "Where the curve has no distortional minimum",
"eq": "at t = %s mm the signature curve shows no distinct distortional minimum (local and "
"global meet), so the target there is checked on local buckling alone; a distortional "
"load would need constrained FSM (the cFSM tab) or a general boundary condition solve" % (
", ".join("%.3g" % t for t in no_d))})
rows = [{"label": "Thicknesses studied", "value": "%d, %.4g to %.4g mm" % (n, t0, t1)},
{"label": "Area at t min", "value": sig(study[0]["A"]), "unit": "mm²"},
{"label": "Py at t max", "value": sig(study[-1]["Py"] / 1000), "unit": "kN"}]
if pick:
cur = [r for r in study if abs(r["t"] - tref) < 1e-9]
lines.append({"label": "Thinnest that meets the target",
"eq": "t = %.4g mm: Pcrl/Py = %s, Pcrd/Py = %s, A = %.5g mm², Py = %.5g kN" % (
pick["t"], "%.4g" % pick["rl"] if pick["rl"] else "n/a", "%.4g" % pick["rd"] if pick["rd"] else "n/a",
pick["A"], pick["Py"] / 1000)})
rows += [{"label": "Thinnest meeting the target", "value": sig(pick["t"]), "unit": "mm", "status": "ok"},
{"label": "Its area", "value": sig(pick["A"]), "unit": "mm²"},
{"label": "Area vs now (t = %.4g mm)" % tref, "value": "%+.1f %%" % (100 * (pick["t"] / tref - 1))}]
m = at_thickness(model, pick["t"], tref)
LAST["model"] = buckling(m, fy, [lengths[0], lengths[-1]])["ref"] # the picked section, stressed at its Py
if inputs.get("offer"):
civilkit.proposeModel(json.dumps(m))
else:
LAST["model"] = None
rows.append({"label": "Thinnest meeting the target", "value": "none in this range: raise t max or lower the target", "status": "warn"})
return {"result": rows, "calcLines": lines}
def build_ui(inputs, result, calcLines):
g = inputs.get
kids = [
{"type": "text", "value": "How local and distortional buckling of this section change with the sheet thickness, "
"and the thinnest thickness that meets a target. Every value is a full engine solve; "
"units N, mm, MPa."},
{"type": "section", "title": "Study", "children": [
{"type": "row", "cols": 2, "children": [
{"type": "field", "id": "t_min", "label": "t min (mm), blank: half of now", "inputType": "number", "default": g("t_min", "")},
{"type": "field", "id": "t_max", "label": "t max (mm), blank: three times now", "inputType": "number", "default": g("t_max", "")},
]},
{"type": "row", "cols": 2, "children": [
{"type": "field", "id": "steps", "label": "Steps", "inputType": "integer", "default": g("steps", 7), "min": 2, "max": 15},
{"type": "field", "id": "target", "label": "Target: min(Pcrl, Pcrd) / Py", "inputType": "number", "default": g("target", 0.5)},
]},
{"type": "field", "id": "fy", "label": "fy (MPa), blank: the analysis's", "inputType": "number", "default": g("fy", "")},
{"type": "field", "id": "offer", "label": "Offer the thinnest passing section to the analysis", "inputType": "boolean", "default": g("offer", False)},
]},
{"type": "button", "id": "run", "label": "Run the study", "action": "run-check"},
]
if result:
kids.append({"type": "result", "items": result})
st = LAST["study"]
if st:
series = [
{"label": "Pcrl / Py (local)", "points": [{"x": r["t"], "y": r["rl"]} for r in st if r["rl"]]},
{"label": "Pcrd / Py (distortional)", "points": [{"x": r["t"], "y": r["rd"]} for r in st if r["rd"]]},
]
ann = [{"x": st[0]["t"], "y": LAST["target"], "label": "target %.3g" % LAST["target"]}]
if LAST["pick"]:
ann.append({"x": LAST["pick"]["t"], "y": LAST["pick"]["worst"], "label": "t = %.3g mm" % LAST["pick"]["t"]})
trend = {"type": "chart", "chart": "line", "title": "Buckling vs thickness", "xLabel": "t (mm)", "yLabel": "Pcr / Py",
"yScale": "log", "markers": True, "legend": True, "grid": True, "ticks": True, "hover": True,
"threshold": LAST["target"], "thresholdLabel": "target", "series": series, "annotations": ann}
sigs = {"type": "chart", "chart": "line", "title": "Signature curves: thinnest, middle, thickest",
"xLabel": "half-wavelength (mm)", "yLabel": "Pcr / Py", "xScale": "log", "yScale": "log",
"legend": True, "grid": True, "ticks": True, "hover": True,
"series": [{"label": "t = %.3g mm" % t, "points": [{"x": p[0], "y": p[1]} for p in c if p[1] > 0]}
for t, c in LAST["curves"]]}
table = {"type": "table", "columns": [
{"key": "t", "label": "t (mm)"}, {"key": "A", "label": "A (mm²)"}, {"key": "Py", "label": "Py (kN)"},
{"key": "Ll", "label": "Lcrl (mm)"}, {"key": "rl", "label": "Pcrl/Py"},
{"key": "Ld", "label": "Lcrd (mm)"}, {"key": "rd", "label": "Pcrd/Py"}, {"key": "ok", "label": ""}],
"rows": [{"t": sig(r["t"]), "A": sig(r["A"]), "Py": sig(r["Py"] / 1000),
"Ll": sig(r["Ll"]) if r["Ll"] else "-", "rl": sig(r["rl"]) if r["rl"] else "-",
"Ld": sig(r["Ld"]) if r["Ld"] else "-", "rd": sig(r["rd"]) if r["rd"] else "-",
"ok": "meets" if r["worst"] is not None and r["worst"] >= LAST["target"] else ""} for r in st]}
views = [{"type": "tab", "label": "Charts", "children": [trend, sigs]},
{"type": "tab", "label": "Table", "children": [table]}]
if LAST["model"]:
views.append({"type": "tab", "label": "The thinnest passing section",
"children": [{"type": "buckling.section", "title": "t = %.4g mm, stressed at its Py" % LAST["pick"]["t"],
"model": LAST["model"], "stress": True}]})
kids.append({"type": "tabs", "children": views})
if calcLines:
kids.append({"type": "calc", "lines": calcLines})
return {"type": "panel", "title": "Thickness study", "children": kids}