Skip to the guide
‹ Back to the app CivilKit Buckling · Guide

Python API reference (cufsm_rs)

Every public name of cufsm_rs, the package pip install cufsm-rs-py installs and the console imports, taken from its code: signatures, arguments, defaults, units and returns. Each entry says how it behaves on your own machine, in full Python in the page and on the console's MicroPython.

NameWhat it is
ModelA cross-section as CUFSM's tables.
section_properties, SectionPropertiesArea, centroid, second moments, J, shear centre, Cw, warping function.
first_yield, YieldActionsThe first-yield actions for a yield stress.
stressReference stresses from member actions.
stress_to_action, StressActionsThe actions that best fit a model's stresses.
signatureThe signature curve and its minima.
stripLoad factors and modes at any lengths, end conditions, terms and cFSM spaces.
StripResultWhat signature and strip return.
ModeShape, DisplacementsOne mode as per-node displacements.
classify, MODE_CLASSEScFSM modal classification: G, D, L, O percentages.
template, lipped_c, lipped_z, plain_cModels from CUFSM's C and Z template.
MechanismErrorThe error for a section with a mechanism.
cufsm_rs.plotplot_section, plot_signature, plot_mode.

Three places, one API#

WhereWhat import cufsm_rs isArraysPlots
Desktop: pip install cufsm-rs-pyThe package, with its compiled native engine (cufsm-rs, built for your machine)numpymatplotlib (pip install "cufsm-rs-py[plot]")
Full Python in the page (Pyodide)The package's own Python code, cufsm-rs-py 0.1.0, unchanged; its native part replaced by calls to the page's cufsm.wasmnumpymatplotlib, shown in the console's output
MicroPython in the pagecufsm_rs lite: the same names, signatures and defaults, written for MicroPython, on the same cufsm.wasmArraydrawn by the page; functions return a Figure

Where this page says array, it means a numpy ndarray on the first two and an Array on MicroPython. The differences on MicroPython are listed under each entry, marked Lite. Things only a notebook uses (the _repr_html_ tables) are the package's and are not shown by the console, which prints the plain repr. cufsm_rs.__version__ is "0.1.0" in the page (pip's current release is 0.1.1, the same API); the lite package also has __lite__ = True.

Units, load factors, end conditions#

Units#

The package is unit-agnostic, as CUFSM is: use one consistent set and every result comes back in it. With N, mm and MPa: coordinates, thicknesses and lengths in mm, moduli and stresses in MPa, forces in N, moments in N·mm, the bimoment in N·mm², areas in mm², I and J in mm⁴, Cw in mm⁶. CUFSM's own tutorial uses kip, in and ksi. The only defaults with units are the templates' E: template's 29500 (ksi), and lipped_c, lipped_z and plain_c's 203000 (MPa, so give those dimensions in mm).

x and z are the cross-section's coordinates; y runs along the member. Positive stress is compression.

Load factors#

A load factor multiplies the model's reference stresses (the node table's last column): the section buckles when those stresses are multiplied by it. Set the reference stresses from the squash load, stress(model, P=y.Py), and a load factor reads directly as Pcr / Py; set them from P = 1 and it reads as Pcr in your force unit.

End conditions#

bcEndsLengths are
"S-S"simply supported (pinned) at both endswith one term (m_all=None): half-wavelengths, the signature curve; with more terms: member lengths
"C-C"clamped (fixed) at both endsmember lengths
"S-C" (or "C-S")simply supported at one end, clamped at the othermember lengths
"C-F" (or "F-C")clamped at one end, free at the othermember lengths
"C-G" (or "G-C")clamped at one end, guided at the othermember lengths

Lower case is accepted; the result's bc is upper case. With general end conditions the modes couple across longitudinal terms, so give enough terms (m_all) to reach the shortest half-wave that matters (Schafer and Li, 2010).

CUFSM's tables#

A model is CUFSM's own arrays, in CUFSM's column order, with its 1-based node and material numbers.

TableColumnsNotes
prop[mat#, Ex, Ey, vx, vy, G]One row per material. An isotropic material has Ex = Ey = E, vx = vy = ν, G = E / (2 (1 + ν)).
node[node#, x, z, xdof, zdof, ydof, qdof, stress]The four DOF flags: 1 free, 0 fixed (x, z, y along the member, θ). stress is the reference stress, positive in compression.
elem[elem#, nodei, nodej, t, mat#]A strip between two nodes, thickness t. With 4 columns (no mat#) every element takes the first material.
constraints[node#e, dofe, coeff, node#k, dofk]DOF e of node e equals coeff times DOF k of node k. DOF codes: 1 x, 2 z, 3 y, 4 θ.
springs[#, nodei, nodej, ku, kv, kw, kq, local, discrete, ys]CUFSM v4.3's form. nodej 0 is ground. local 1 orients a spring between two nodes along the line joining them (else ku acts along x and kw along z). discrete 0 is a foundation spring, a stiffness per unit length; 1 is a spring at one point, ys the fraction of the length where it acts.

Mode vectors use CUFSM's DOF order: for each longitudinal term in turn, a block of 4 × nodes values, first u and v interleaved for every node (u1 v1 u2 v2 …), then w and θ interleaved (w1 θ1 w2 θ2 …). Each mode is scaled so its largest entry is +1.

Model#

Model(prop, node, elem, constraints=None, springs=None)

A cross-section held as CUFSM's tables. Each argument is a list of rows (or a numpy array); one row may be given flat. prop, node and elem need at least one row each; elem takes 4 or 5 columns. A wrong shape raises ValueError (node must be a 2-D array with 8 columns, got shape (3, 7)). Numbers that refer to nothing (an element's node that is not in node) are refused when the model is first analysed.

MemberWhat it is
prop, node, elem, constraints, springsThe tables, as arrays of shape (rows, 6), (rows, 8), (rows, 5), (rows, 5) and (rows, 10); editable in place (m.node[3][7] = 0.5). Empty constraints and springs have 0 rows.
x, z, stressNode columns 2, 3 and 8 (coordinates and reference stresses), as views: writing to one writes the table.
Model.from_dicts(nodes, elements, materials=None, E=None, nu=0.3, constraints=(), springs=())A model from lists of dicts with 0-based indices. nodes: {"x", "z", "stress"=1.0, "free"=(1, 1, 1, 1)} (free flags for x, z, y, θ). elements: {"i", "j", "t", "mat"=0}. materials: {"E", "nu"} or {"Ex", "Ey", "vx", "vy", "G"}; or give E (and nu) for one isotropic material (neither raises ValueError: give materials or E). constraints: {"node_e", "dof_e", "coeff"=1.0, "node_k", "dof_k"}, DOFs as 1 to 4 or "x", "z", "y", "q" (or "theta"). springs: {"i", "j"=None (ground), "ku", "kv", "kw", "kq"=0, "local"=False, "discrete"=False, "ys"=0}.
copy()A deep copy.
with_stress(s)A copy with the reference stresses replaced by s, one value per node (else ValueError).
section_properties(), first_yield(fy, restrained=False, extreme_fibre=True), signature(lengths=None, neigs=1), strip(lengths, m_all=None, bc="S-S", neigs=20, spaces=None)The module functions as methods: m.signature() is signature(m).

repr: Model(10 nodes, 9 elements, 1 materials, 0 constraints, 0 springs). Lite: the tables are Arrays and x, z, stress writable column views; index them m.node[i][j], not m.node[i, j].

import cufsm_rs as fsm

# CUFSM's tutorial C, 9 x 5 x 1 in, t = 0.1 in (kip, in, ksi)
xz = [(5, 1), (5, 0), (2.5, 0), (0, 0), (0, 3), (0, 6), (0, 9), (2.5, 9), (5, 9), (5, 8)]
m = fsm.Model(
    prop=[[100, 29500, 29500, 0.3, 0.3, 11346.15]],
    node=[[i + 1, x, z, 1, 1, 1, 1, 0] for i, (x, z) in enumerate(xz)],
    elem=[[i + 1, i + 1, i + 2, 0.1, 100] for i in range(9)],
)
print(m)

# the same channel from dicts, 0-based
m2 = fsm.Model.from_dicts(
    nodes=[{"x": x, "z": z} for x, z in xz],
    elements=[{"i": k, "j": k + 1, "t": 0.1} for k in range(9)],
    E=29500, nu=0.3,
)
print(m2.node[0][7], len(m2.x))      # the default reference stress 1.0, and 10 nodes

Properties, yield and stresses#

section_properties(model)#

Gross and thin-walled section properties (CUFSM's grosprop and cutwp_prop2). Returns a SectionProperties.

SectionProperties#

A frozen record, read as attributes (p.A) or as a mapping (p["A"], p.keys(), p.as_dict()). Units for N, mm, MPa:

FieldWhat it isUnit
Aareamm²
xcg, zcgcentroidmm
Ixx, Izz, Ixzsecond moments of area about centroidal axes parallel to x and zmm⁴
thetapangle of the principal axes from xdegrees
I11, I22principal second moments of areamm⁴
JSt Venant torsion constantmm⁴
xs, zsshear centremm
Cwwarping constantmm⁶
B1, B2monosymmetry (Wagner) parameters about principal axes 1 and 2mm
wnnormalised unit warping at each node, in node order (an array)mm²

For a closed cell the warping quantities (xs, zs, Cw, B1, B2, wn) are NaN, as in CUFSM.

first_yield(model, fy, restrained=False, extreme_fibre=True)#

The first-yield actions for yield stress fy (in the model's stress unit): each action alone that first brings a fibre to fy. extreme_fibre=True (the default, as current CUFSM) checks the element faces (yieldMP_extfiber); False checks the centreline nodes (yieldMP). restrained=True is restrained bending (Ixz ignored). fy must be positive and finite, else ValueError. Returns a YieldActions.

YieldActions#

A frozen record, also a mapping: fy (the yield stress used), Py (squash load, A fy, N), Mxx, Mzz (about the centroidal axes parallel to x and z, N·mm), M11, M22 (about the principal axes, N·mm), B (the bimoment, CUFSM's yieldB, N·mm²).

stress(model, P=0.0, Mxx=0.0, Mzz=0.0, M11=0.0, M22=0.0, B=0.0, restrained=False, as_array=False)#

Reference stresses from member actions (CUFSM's stresgen, plus warp_stress for a bimoment). P axial force, positive in compression; Mxx, Mzz moments about the centroidal axes parallel to x and z; M11, M22 about the principal axes; B a bimoment. The actions add. The model's own stress column is ignored. Returns a new Model with the stresses set (the model passed in is unchanged), or with as_array=True the stresses alone, one per node.

stress_to_action(model)#

The P, M11, M22 and B whose stresses best fit the model's nodal stresses, by least squares (CUFSM's stress_to_action). Returns a StressActions.

StressActions#

A frozen record, also a mapping: P, M11, M22, B, and err, the fit's residual norm (a stress).

import cufsm_rs as fsm

m = fsm.lipped_c(150, 64, 15, 1.5)          # mm; E = 203000 MPa
p = fsm.section_properties(m)
print("A = %.1f mm2, Ixx = %.4g mm4, Cw = %.4g mm6, xs = %.2f mm" % (p.A, p.Ixx, p.Cw, p.xs))
y = fsm.first_yield(m, fy=450)
print("Py = %.1f kN, Mxx = %.2f kNm" % (y.Py / 1e3, y.Mxx / 1e6))
mb = fsm.stress(m, Mxx=y.Mxx)                # bending about x at first yield
a = fsm.stress_to_action(mb)
print("fitted M11 = %.4g N.mm, residual %.2g" % (a.M11, a.err))

Analysis: signature and strip#

signature(model, lengths=None, neigs=1)#

The signature curve: S-S ends, one half-wave at each half-wavelength. lengths are half-wavelengths in the model's length unit (any sequence; it is flattened); None gives CUFSM's signature_ss 100 log-spaced lengths for this model. neigs is how many load factors to find at each length. Returns a StripResult with kind = "signature" and its minima filled: the interior local minima of the lowest curve, shortest first, refined by a parabola in log length. Each length must be positive and finite.

strip(model, lengths, m_all=None, bc="S-S", neigs=20, spaces=None)#

CUFSM's stripmain: load factors and modes at each length.

ArgumentDefaultMeaning
lengths(required)Half-wavelengths for S-S with one term; member lengths otherwise. Model length unit.
m_allNoneThe longitudinal terms at each length: None for [1]; an int n for 1..n; one list ([1, 3, 5]) used at every length; or a list of lists, one per length (else ValueError: 3 lengths but 2 sets of longitudinal terms). Each list is sorted and its zeros and repeats dropped, as CUFSM's msort; m_terms reports what was used.
bc"S-S"The end conditions.
neigs20How many load factors to find at each length, smallest first. Fewer may be found (the rest are NaN).
spacesNoneRestrict the analysis to cFSM spaces: a string of the letters G, D, L, O in either case, e.g. "D" for pure distortional or "GD" for the union of global and distortional (constrained FSM). None is the unrestricted analysis.

Returns a StripResult with kind = "strip" and empty minima. There is no fixed limit on neigs or the number of terms; memory is the limit (the solve holds dense matrices of (4 × nodes × terms)² values).

import cufsm_rs as fsm

m = fsm.lipped_c(150, 64, 15, 1.5)
mc = fsm.stress(m, P=fsm.first_yield(m, fy=450).Py)    # load factor = Pcr / Py

sig = fsm.signature(mc)
print(sig)
for L, lf in sig.minima:
    print("minimum at %.0f mm: %.3f Py" % (L, lf))

dist = fsm.strip(mc, [400.0], spaces="D", neigs=1)     # pure distortional (cFSM)
print("D-only at 400 mm: %.3f Py" % dist.load_factors[0][0])

cc = fsm.strip(mc, [1000.0, 2000.0], m_all=12, bc="C-C", neigs=2)   # fixed ends
print("C-C:", [round(cc.curve[i], 4) for i in range(len(cc.curve))])

StripResult#

What signature and strip return.

MemberWhat it is
modelThe analysed Model.
bcThe end conditions, upper case.
kind"signature" or "strip".
lengths(nlengths,) the lengths analysed.
m_termsA list with one (nterms,) array per length: the terms used there.
load_factors(nlengths, neigs), smallest first at each length; NaN where fewer were found.
modes(nlengths, neigs, ndof) raw mode vectors in CUFSM's DOF order, NaN-padded. Use mode_shape for per-node arrays.
minima(nminima, 2) rows of [length, load factor] at the interior local minima, shortest first. signature only; empty for strip.
curveThe lowest load factor at each length, load_factors[:, 0].
neigsThe number of load-factor columns.
mode_shape(i_length, k_mode=0)Mode k_mode (0 is the lowest) at lengths[i_length], as a ModeShape. Negative i_length counts from the end. IndexError when out of range or when that mode was not found.
classify(orth="axial", norm="vector", ospace="st")classify(self, …): (nlengths, neigs, 4) percentages [G, D, L, O].
classify_minima()[G, D, L, O] percentages of the lowest mode at each of minima, shape (nminima, 4): each minimum's length is solved again (S-S, one term) and classified with CUFSM's defaults.

repr: StripResult(signature, bc=S-S, 80 lengths 1 to 1000, neigs=1, 2 minima). Lite: every array is an Array, so curve is a new Array built from load_factors, and load_factors[:, 0] raises; use r.curve or r.load_factors[i][0].

ModeShape#

One buckling mode split into per-node displacement amplitudes, from StripResult.mode_shape. The finite strip field is, for each longitudinal term m with shape function Ym (sin(mπy/a) for S-S) and length a:

u(x, y) = Σ u_m Y_m(y)                 along x
w(x, y) = Σ w_m Y_m(y)                 along z
θ       = Σ θ_m Y_m(y)
v(x, y) = Σ v_m Y_m'(y) a / (m π)      along y, the member axis
FieldWhat it is
length, load_factor, bcThe length a, the mode's load factor and the end conditions.
m_terms(nterms,) the longitudinal terms.
x, z(nnodes,) the undeformed node coordinates.
u, v, w, theta(nterms, nnodes) amplitudes per term and node. theta is the rotation in the x–z plane, positive anticlockwise with x to the right and z up.
dofs(4 × nnodes × nterms,) the raw vector. For term block t and node i, with n nodes and o = 4nt: u = dofs[o + 2i], v = dofs[o + 2i + 1], w = dofs[o + 2n + 2i], theta = dofs[o + 2n + 2i + 1]. Scaled so its largest entry is +1: displacements are relative, not absolute.
at(y=None)The nodal displacements at y along the member (0 to length), summed over the terms, as Displacements. With None, where the in-plane displacement (u, w) is largest: mid-length for one S-S term.

Displacements#

A named tuple (y, u, v, w, theta): the position, and one value per node for each displacement.

import cufsm_rs as fsm

m = fsm.lipped_c(150, 64, 15, 1.5)
mc = fsm.stress(m, P=1.0)
r = fsm.strip(mc, [400.0], neigs=3)
shape = r.mode_shape(0)                 # the lowest mode at 400 mm
d = shape.at()                          # summed over terms, where it is largest
big = max(range(len(d.u)), key=lambda i: abs(d.u[i]) + abs(d.w[i]))
print("load factor %.4g: node %d moves most (u %.3f, w %.3f)" % (shape.load_factor, big + 1, d.u[big], d.w[big]))

classify(result, orth="axial", norm="vector", ospace="st")#

cFSM modal classification (CUFSM's classify.m, uncoupled basis). Returns (nlengths, neigs, 4) percentages [G, D, L, O] in MODE_CLASSES order, NaN-padded where fewer modes were found. The options, each case-insensitive; the defaults are CUFSM's:

OptionValues
orth"natural", "axial", "load" (the basis's orthogonalisation)
norm"none", "vector", "strain_energy", "work" (the normalisation)
ospace"st", "k", "kg", "vector" (how the O space is formed)

A few combinations CUFSM itself leaves undefined are refused with a ValueError, as the package refuses them. result.classify(…) is the same call.

MODE_CLASSES#

("G", "D", "L", "O"): global, distortional, local, other, the order of every classification.

import cufsm_rs as fsm

mc = fsm.stress(fsm.lipped_c(150, 64, 15, 1.5), P=1.0)
r = fsm.strip(mc, [60.0, 400.0, 3000.0], neigs=1)
for i, per in enumerate(r.classify()):
    c = per[0]
    k = max(range(4), key=lambda j: c[j])
    print("%5.0f mm: %s %.0f%%" % (r.lengths[i], fsm.MODE_CLASSES[k], c[k]))

Templates#

template(shape="C", h=9.0, b1=5.0, b2=None, d1=1.0, d2=None, r1=0.0, r2=None, r3=None, r4=None, q1=90.0, q2=None, t=0.1, nh=4, nb1=2, nb2=None, nd1=None, nd2=None, nr1=None, nr2=None, nr3=None, nr4=None, centerline=True, E=29500.0, nu=0.3)#

CUFSM's C and Z template (templatecalc), meshed by the engine. shape "C" or "Z"; h web depth; b1, b2 flange widths; d1, d2 lip lengths (0 for none); r1 to r4 corner radii; q1, q2 lip angles in degrees; t thickness; nh, nb1, nb2, nd1, nd2, nr1 to nr4 strip counts; centerline whether the dimensions are to the centreline (else outside dimensions and inside radii). A second-side value left as None takes the first side's; a lip or corner strip count left as None is 2 where that lip or radius is non-zero, else 0. The defaults are CUFSM's, in inches and ksi. Returns a Model with one isotropic material (E, nu), every node free and a reference stress of 1.0 at every node.

lipped_c(depth, flange, lip, t, ri=0.0, mesh=12, E=203000.0, nu=0.3)#

A lipped channel from outside dimensions and inside radius ri: mesh strips across the web, half that (at least 2) across each flange, two per lip and two per corner (when ri > 0). The default E is steel in MPa, so give the dimensions in mm.

lipped_z(depth, flange, lip, t, ri=0.0, mesh=12, E=203000.0, nu=0.3)#

A lipped Z, as lipped_c.

plain_c(depth, flange, t, ri=0.0, mesh=12, E=203000.0, nu=0.3)#

A plain (unlipped) channel, as lipped_c with no lip.

import cufsm_rs as fsm

lc = fsm.lipped_c(200, 76, 15, 1.9, ri=3)    # mm
print(lc)                                     # 37 nodes
z = fsm.lipped_z(200, 76, 15, 1.9, ri=3)
pc = fsm.plain_c(100, 50, 2.0)
tc = fsm.template("C", h=9, b1=5, d1=1, t=0.1) # CUFSM's default C, inches
print(len(z.node), len(pc.node), len(tc.node))

cufsm_rs.plot#

Three plots. With pip they need matplotlib (pip install "cufsm-rs-py[plot]"); import cufsm_rs alone never imports it. In the page they need nothing extra on MicroPython, and on full Python a script that uses them downloads matplotlib with the rest.

plot_section(model, ax=None, node_numbers=False, stress=True)#

The cross-section: elements drawn to their thickness, the nodes, and with stress=True the reference stress, compression and tension in their own colours. node_numbers=True labels the nodes with their CUFSM numbers.

plot_signature(result, ax=None, classify=False)#

Load factor against length on a log x axis, the minima marked and labelled (0.353 at 7.27). classify=True also labels each minimum with its dominant cFSM class from classify_minima(), and the runner-up when it is 25 % or more (D 54% L 45%).

plot_mode(result, i_length, k_mode=0, ax=None, scale=None, y=None)#

A mode's deformed cross-section over the undeformed one, cut at y along the member (default: where the in-plane displacement is largest). scale multiplies the displacements; by default the largest in-plane displacement is drawn at 10 % of the section size, whatever the units. Titled mode 1: load factor … at length ….

pip and full PythonLite (MicroPython)
Draws withmatplotlibthe page's own drawing code, in the console's output
Returnsthe matplotlib Axesa Figure: its kind ("section", "signature" or "mode") and title; any Axes method on it raises, naming the full runtime
ax=draws on those axes (a new figure's when None)must be None; anything else raises NotImplementedError (and the console's fallback moves the script to full Python)
When it appearspip: on plt.show() or in a notebook. In the page: at plt.show(), or at the end of the script for figures still openwhere the script calls it
Looka stress band beside each element; strips bent with their end rotations; linear y axisstrips shaded by stress; strips straight between nodes; log y axis when every load factor is positive

Every number drawn is one the script holds: the engine's results, the minima and classify_minima(), and the mode's displacements from ModeShape.at().

import cufsm_rs as fsm
from cufsm_rs.plot import plot_section, plot_signature, plot_mode

m = fsm.lipped_c(150, 64, 15, 1.5)
mc = fsm.stress(m, P=fsm.first_yield(m, fy=450).Py)
sig = fsm.signature(mc)
plot_section(mc, node_numbers=True)
plot_signature(sig, classify=True)
plot_mode(sig, i_length=40)

Errors#

ErrorWhen
ValueErrorInvalid input; the message names the bad row or argument (elem row 3 refers to node 12, which is not in node, fy = 0 must be positive).
MechanismErrorA subclass of ValueError: the elastic stiffness is not positive definite, the section has a mechanism at that length.
TypeErrorA function that takes a model was given something else (expected a cufsm_rs.Model, got list).
IndexErrormode_shape out of range, or asked for a mode that was not found.
NotImplementedErrorLite only: something that needs numpy or matplotlib (whole-array arithmetic, a[i, j], ax=). The message names the full runtime, and the console re-runs the script there.
MemoryErrorIn the page: an output larger than the engine's memory can hold.
RuntimeErrorIn the page: the engine stopped on an internal error (pip raises PanicException there). The console carries on; the next call works.

Credit: CUFSM is by B.W. Schafer and co-workers at Johns Hopkins University; cufsm-rs and this package are an independent port, not affiliated with or endorsed by the CUFSM authors. pyCUFSM, by ClearCalcs, is the earlier pure-Python port and prior work this project gratefully acknowledges. To publish results, cite CUFSM as its authors ask: Schafer, B.W., Ádány, S., Li, Z., Jin, S., CUFSM v5.66, DOI 10.5281/zenodo.17771486; and for general end conditions, Schafer, B.W., Li, Z., "Buckling analysis of cold-formed steel members with general boundary conditions using CUFSM: conventional and constrained finite strip methods", 20th International Specialty Conference on Cold-Formed Steel Structures, 2010, pp. 17-32.

CivilKit Buckling runs cufsm-rs, a Rust port of CUFSM, the finite strip software by Benjamin W. Schafer and co-workers (constrained FSM with Sándor Ádány), compiled to WebAssembly. An analysis tool, not a design certificate: check what you use.