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.
| Name | What it is |
|---|---|
Model | A cross-section as CUFSM's tables. |
section_properties, SectionProperties | Area, centroid, second moments, J, shear centre, Cw, warping function. |
first_yield, YieldActions | The first-yield actions for a yield stress. |
stress | Reference stresses from member actions. |
stress_to_action, StressActions | The actions that best fit a model's stresses. |
signature | The signature curve and its minima. |
strip | Load factors and modes at any lengths, end conditions, terms and cFSM spaces. |
StripResult | What signature and strip return. |
ModeShape, Displacements | One mode as per-node displacements. |
classify, MODE_CLASSES | cFSM modal classification: G, D, L, O percentages. |
template, lipped_c, lipped_z, plain_c | Models from CUFSM's C and Z template. |
MechanismError | The error for a section with a mechanism. |
cufsm_rs.plot | plot_section, plot_signature, plot_mode. |
Three places, one API#
| Where | What import cufsm_rs is | Arrays | Plots |
|---|---|---|---|
Desktop: pip install cufsm-rs-py | The package, with its compiled native engine (cufsm-rs, built for your machine) | numpy | matplotlib (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.wasm | numpy | matplotlib, shown in the console's output |
| MicroPython in the page | cufsm_rs lite: the same names, signatures and defaults, written for MicroPython, on the same cufsm.wasm | Array | drawn 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#
bc | Ends | Lengths are |
|---|---|---|
"S-S" | simply supported (pinned) at both ends | with one term (m_all=None): half-wavelengths, the signature curve; with more terms: member lengths |
"C-C" | clamped (fixed) at both ends | member lengths |
"S-C" (or "C-S") | simply supported at one end, clamped at the other | member lengths |
"C-F" (or "F-C") | clamped at one end, free at the other | member lengths |
"C-G" (or "G-C") | clamped at one end, guided at the other | member 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.
| Table | Columns | Notes |
|---|---|---|
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.
| Member | What it is |
|---|---|
prop, node, elem, constraints, springs | The 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, stress | Node 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:
| Field | What it is | Unit |
|---|---|---|
A | area | mm² |
xcg, zcg | centroid | mm |
Ixx, Izz, Ixz | second moments of area about centroidal axes parallel to x and z | mm⁴ |
thetap | angle of the principal axes from x | degrees |
I11, I22 | principal second moments of area | mm⁴ |
J | St Venant torsion constant | mm⁴ |
xs, zs | shear centre | mm |
Cw | warping constant | mm⁶ |
B1, B2 | monosymmetry (Wagner) parameters about principal axes 1 and 2 | mm |
wn | normalised 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.
| Argument | Default | Meaning |
|---|---|---|
lengths | (required) | Half-wavelengths for S-S with one term; member lengths otherwise. Model length unit. |
m_all | None | The 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. |
neigs | 20 | How many load factors to find at each length, smallest first. Fewer may be found (the rest are NaN). |
spaces | None | Restrict 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.
| Member | What it is |
|---|---|
model | The analysed Model. |
bc | The end conditions, upper case. |
kind | "signature" or "strip". |
lengths | (nlengths,) the lengths analysed. |
m_terms | A 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. |
curve | The lowest load factor at each length, load_factors[:, 0]. |
neigs | The 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
| Field | What it is |
|---|---|
length, load_factor, bc | The 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:
| Option | Values |
|---|---|
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 Python | Lite (MicroPython) | |
|---|---|---|
| Draws with | matplotlib | the page's own drawing code, in the console's output |
| Returns | the matplotlib Axes | a 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 appears | pip: on plt.show() or in a notebook. In the page: at plt.show(), or at the end of the script for figures still open | where the script calls it |
| Look | a stress band beside each element; strips bent with their end rotations; linear y axis | strips 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#
| Error | When |
|---|---|
ValueError | Invalid 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). |
MechanismError | A subclass of ValueError: the elastic stiffness is not positive definite, the section has a mechanism at that length. |
TypeError | A function that takes a model was given something else (expected a cufsm_rs.Model, got list). |
IndexError | mode_shape out of range, or asked for a mode that was not found. |
NotImplementedError | Lite 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. |
MemoryError | In the page: an output larger than the engine's memory can hold. |
RuntimeError | In 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.