Truss analysis#
In this notebook, we solve a truss analysis problem: computing forces and deformations in a 2D pin-jointed frame under load.
Features used:
Varsubclassing for node displacement variables@jaxls.Cost.factoryfor bar element strain energyEquality constraints for fixed supports
Batched cost construction for all members
This is a classic introductory finite element analysis (FEA) problem using 1D bar elements.
import jax
import jax.numpy as jnp
import jaxls
Truss element theory#
A truss is a structure of bar elements connected at pin joints (nodes). Each bar:
Carries only axial force (tension or compression)
Has stiffness \(k = \frac{EA}{L}\) where \(E\) is Young’s modulus, \(A\) is cross-sectional area, \(L\) is length
The strain energy in a bar element is
where \(L'\) is the deformed length.
class NodeVar(jaxls.Var[jax.Array], default_factory=lambda: jnp.zeros(2)):
"""2D node displacement variable [dx, dy] in meters."""
Cost functions#
Bar strain energy: Penalizes elongation/compression of each member
Support constraints: Fix displacements at support nodes
Load application: Prescribe displacement at load point (equilibrium is automatic)
@jaxls.Cost.factory
def bar_strain_energy(
vals: jaxls.VarValues,
node_i: NodeVar,
node_j: NodeVar,
pos_i: jax.Array,
pos_j: jax.Array,
EA: float,
) -> jax.Array:
"""Strain energy in a bar element: (1/2) * EA/L * (delta_L)^2.
Args:
node_i, node_j: Displacement variables at each end.
pos_i, pos_j: Initial (undeformed) positions.
EA: Axial stiffness (Young's modulus × area).
"""
# Initial geometry.
L0_vec = pos_j - pos_i
L0 = jnp.sqrt(jnp.sum(L0_vec**2))
# Deformed geometry.
disp_i = vals[node_i]
disp_j = vals[node_j]
L_vec = L0_vec + (disp_j - disp_i)
L = jnp.sqrt(jnp.sum(L_vec**2))
# Return 2D residual instead of scalar: ||r||^2 = (EA/L0) * (L - L0)^2.
# Using a 2D residual gives a rank-2 contribution to J^T J (the Gauss-Newton.
# Hessian approximation), rather than rank-1 from a scalar residual.
return jnp.sqrt(EA / L0) * (1 - L0 / L) * L_vec
@jaxls.Cost.factory(kind="constraint_eq_zero")
def pin_support(
vals: jaxls.VarValues,
node: NodeVar,
) -> jax.Array:
"""Pin support: both displacement components are zero."""
return vals[node]
@jaxls.Cost.factory(kind="constraint_eq_zero")
def prescribed_displacement(
vals: jaxls.VarValues,
node: NodeVar,
target_displacement: jax.Array,
) -> jax.Array:
"""Prescribe displacement at a node."""
return vals[node] - target_displacement
Truss geometry#
We model a Warren truss, a common bridge structure with diagonal members:
5-----6-----7-----8
/\ /\ /\ /\
/ \ / \ / \ / \
/ \/ \/ \/ \
0-----1-----2-----3-----4
Nodes 0-4: Bottom chord
Nodes 5-8: Top chord
Nodes 0 and 4 are pinned (fixed in x and y)
Load applied at center bottom node (node 2)
# Geometry: Warren truss bridge.
num_panels = 4 # Number of triangular panels
panel_width = 3.0 # [m] width of each panel
height = 2.0 # [m] truss height
span = num_panels * panel_width # Total span
# Build node positions.
bottom_nodes = [[i * panel_width, 0.0] for i in range(num_panels + 1)]
top_nodes = [[(i + 0.5) * panel_width, height] for i in range(num_panels)]
node_positions = jnp.array(bottom_nodes + top_nodes)
num_nodes = len(node_positions)
# Node indices.
bottom_ids = list(range(num_panels + 1)) # 0, 1, 2, 3, 4
top_ids = list(range(num_panels + 1, num_nodes)) # 5, 6, 7, 8
# Build member connectivity.
member_list = []
# Bottom chord.
for i in range(num_panels):
member_list.append([bottom_ids[i], bottom_ids[i + 1]])
# Top chord.
for i in range(num_panels - 1):
member_list.append([top_ids[i], top_ids[i + 1]])
# Diagonals (left and right of each top node)
for i in range(num_panels):
member_list.append([bottom_ids[i], top_ids[i]]) # Left diagonal
member_list.append([top_ids[i], bottom_ids[i + 1]]) # Right diagonal
members = jnp.array(member_list)
num_members = len(members)
# Material properties.
EA = 50000.0 # [N] axial stiffness
# Load node.
load_node_id = 2 # Center bottom node
# Prescribed displacement (downward)
load_displacement = jnp.array([0.0, -0.02]) # 20 mm downward
print("Warren Truss Bridge:")
print(f" Span: {span} m, Height: {height} m")
print(f" Nodes: {num_nodes}, Members: {num_members}")
print(f" Member stiffness EA = {EA:.0f} N")
print(
f" Prescribed displacement at node {load_node_id}: {float(load_displacement[1]) * 1000:.1f} mm (vertical)"
)
Warren Truss Bridge:
Span: 12.0 m, Height: 2.0 m
Nodes: 9, Members: 15
Member stiffness EA = 50000 N
Prescribed displacement at node 2: -20.0 mm (vertical)
Problem construction#
# Create node displacement variables.
node_vars = NodeVar(id=jnp.arange(num_nodes))
# Support nodes (both pinned).
left_pin = 0
right_pin = num_panels
# Build costs.
costs: list[jaxls.Cost] = [
# Strain energy in all members (batched).
bar_strain_energy(
NodeVar(id=members[:, 0]),
NodeVar(id=members[:, 1]),
node_positions[members[:, 0]],
node_positions[members[:, 1]],
EA,
),
# Boundary conditions.
pin_support(NodeVar(id=left_pin)),
pin_support(NodeVar(id=right_pin)),
# Applied load via prescribed displacement.
prescribed_displacement(NodeVar(id=load_node_id), load_displacement),
]
print(f"Created {len(costs)} cost objects")
print(f"Load applied at node {load_node_id}")
Created 4 cost objects
Load applied at node 2
Solving#
# Initial values: zero displacement.
initial_displacements = jnp.zeros((num_nodes, 2))
initial_vals = jaxls.VarValues.make([node_vars.with_value(initial_displacements)])
# Build the problem.
problem = jaxls.LeastSquaresProblem(costs, [node_vars])
# Visualize the problem structure structure.
problem.show()
# Analyze and solve.
problem = problem.analyze()
solution = problem.solve(initial_vals)
INFO | Building optimization problem with 18 terms and 9 variables: 15 costs, 3 eq_zero, 0 leq_zero, 0 geq_zero
INFO | Vectorizing constraint group with 2 constraints (constraint_eq_zero), 1 variables each: augmented_pin_support
INFO | Vectorizing constraint group with 1 constraints (constraint_eq_zero), 1 variables each: augmented_prescribed_displacement
INFO | Vectorizing group with 15 costs, 2 variables each: bar_strain_energy
INFO | Augmented Lagrangian: initial snorm=2.0000e-02, csupn=2.0000e-02, max_rho=1.0000e+01, constraint_dim=6
INFO | step #0: cost=0.0000 lambd=0.0005 inexact_tol=1.0e-02
INFO | - augmented_pin_support(2): 0.00000 (avg 0.00000)
INFO | - augmented_prescribed_displacement(1): 0.00400 (avg 0.00200)
INFO | - bar_strain_energy(15): 0.00000 (avg 0.00000)
INFO | accepted=True ATb_norm=2.02e-01 cost_prev=0.0040 cost_new=0.0027
INFO | step #1: cost=0.0000 lambd=0.0003 inexact_tol=1.0e-02
INFO | - augmented_pin_support(2): 0.00088 (avg 0.00022)
INFO | - augmented_prescribed_displacement(1): 0.00177 (avg 0.00088)
INFO | - bar_strain_energy(15): 0.00001 (avg 0.00000)
INFO | accepted=True ATb_norm=8.03e-03 cost_prev=0.0027 cost_new=0.0027
INFO | AL update: snorm=1.3288e-02, csupn=1.3288e-02, max_rho=4.0000e+01
INFO | step #2: cost=0.0000 lambd=0.0001 inexact_tol=1.4e-03
INFO | - augmented_pin_support(2): 0.00552 (avg 0.00138)
INFO | - augmented_prescribed_displacement(1): 0.01104 (avg 0.00552)
INFO | - bar_strain_energy(15): 0.00001 (avg 0.00000)
INFO | accepted=True ATb_norm=6.56e-01 cost_prev=0.0166 cost_new=0.0164
INFO | step #3: cost=0.0002 lambd=0.0001 inexact_tol=1.4e-03
INFO | - augmented_pin_support(2): 0.00540 (avg 0.00135)
INFO | - augmented_prescribed_displacement(1): 0.01079 (avg 0.00540)
INFO | - bar_strain_energy(15): 0.00023 (avg 0.00001)
INFO | accepted=True ATb_norm=8.96e-03 cost_prev=0.0164 cost_new=0.0164
INFO | AL update: snorm=1.3102e-02, csupn=1.3102e-02, max_rho=1.6000e+02
INFO | step #4: cost=0.0002 lambd=0.0000 inexact_tol=1.7e-04
INFO | - augmented_pin_support(2): 0.02369 (avg 0.00592)
INFO | - augmented_prescribed_displacement(1): 0.04738 (avg 0.02369)
INFO | - bar_strain_energy(15): 0.00023 (avg 0.00001)
INFO | accepted=True ATb_norm=2.59e+00 cost_prev=0.0713 cost_new=0.0691
INFO | step #5: cost=0.0036 lambd=0.0000 inexact_tol=1.7e-04
INFO | - augmented_pin_support(2): 0.02184 (avg 0.00546)
INFO | - augmented_prescribed_displacement(1): 0.04364 (avg 0.02182)
INFO | - bar_strain_energy(15): 0.00365 (avg 0.00012)
INFO | accepted=True ATb_norm=6.89e-03 cost_prev=0.0691 cost_new=0.0691
INFO | AL update: snorm=1.2409e-02, csupn=1.2409e-02, max_rho=6.4000e+02
INFO | step #6: cost=0.0036 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.08763 (avg 0.02191)
INFO | - augmented_prescribed_displacement(1): 0.17503 (avg 0.08751)
INFO | - bar_strain_energy(15): 0.00365 (avg 0.00012)
INFO | accepted=True ATb_norm=9.80e+00 cost_prev=0.2663 cost_new=0.2399
INFO | step #7: cost=0.0425 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.06635 (avg 0.01659)
INFO | - augmented_prescribed_displacement(1): 0.13097 (avg 0.06548)
INFO | - bar_strain_energy(15): 0.04254 (avg 0.00142)
INFO | accepted=False ATb_norm=6.01e-02 cost_prev=0.2399 cost_new=0.2399
INFO | AL update: snorm=1.0176e-02, csupn=1.0176e-02, max_rho=2.5600e+03
INFO | step #8: cost=0.0425 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.24687 (avg 0.06172)
INFO | - augmented_prescribed_displacement(1): 0.48419 (avg 0.24210)
INFO | - bar_strain_energy(15): 0.04254 (avg 0.00142)
INFO | accepted=True ATb_norm=3.22e+01 cost_prev=0.7736 cost_new=0.5982
INFO | step #9: cost=0.2582 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.12402 (avg 0.03101)
INFO | - augmented_prescribed_displacement(1): 0.21590 (avg 0.10795)
INFO | - bar_strain_energy(15): 0.25824 (avg 0.00861)
INFO | accepted=True ATb_norm=1.81e-01 cost_prev=0.5982 cost_new=0.5981
INFO | AL update: snorm=5.6058e-03, csupn=5.6058e-03, max_rho=1.0240e+04
INFO | step #10: cost=0.2583 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.40213 (avg 0.10053)
INFO | - augmented_prescribed_displacement(1): 0.63929 (avg 0.31965)
INFO | - bar_strain_energy(15): 0.25834 (avg 0.00861)
INFO | accepted=True ATb_norm=7.44e+01 cost_prev=1.2998 cost_new=0.9458
INFO | step #11: cost=0.6498 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.14261 (avg 0.03565)
INFO | - augmented_prescribed_displacement(1): 0.15344 (avg 0.07672)
INFO | - bar_strain_energy(15): 0.64977 (avg 0.02166)
INFO | accepted=False ATb_norm=1.49e-01 cost_prev=0.9458 cost_new=0.9459
INFO | AL update: snorm=1.5755e-03, csupn=1.5755e-03, max_rho=1.0240e+04
INFO | step #12: cost=0.6498 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.35443 (avg 0.08861)
INFO | - augmented_prescribed_displacement(1): 0.30377 (avg 0.15188)
INFO | - bar_strain_energy(15): 0.64977 (avg 0.02166)
INFO | accepted=True ATb_norm=2.80e+01 cost_prev=1.3080 cost_new=1.2693
INFO | step #13: cost=0.8255 lambd=0.0000 inexact_tol=6.4e-06
INFO | - augmented_pin_support(2): 0.24262 (avg 0.06066)
INFO | - augmented_prescribed_displacement(1): 0.20120 (avg 0.10060)
INFO | - bar_strain_energy(15): 0.82545 (avg 0.02752)
INFO | accepted=True ATb_norm=3.85e-02 cost_prev=1.2693 cost_new=1.2692
INFO | AL update: snorm=8.3995e-04, csupn=8.3995e-04, max_rho=4.0960e+04
INFO | step #14: cost=0.8254 lambd=0.0000 inexact_tol=1.7e-06
INFO | - augmented_pin_support(2): 0.24099 (avg 0.06025)
INFO | - augmented_prescribed_displacement(1): 0.25535 (avg 0.12768)
INFO | - bar_strain_energy(15): 0.82543 (avg 0.02751)
INFO | accepted=True ATb_norm=5.20e+01 cost_prev=1.3218 cost_new=1.2709
INFO | step #15: cost=0.9478 lambd=0.0000 inexact_tol=1.7e-06
INFO | - augmented_pin_support(2): 0.08918 (avg 0.02229)
INFO | - augmented_prescribed_displacement(1): 0.23397 (avg 0.11698)
INFO | - bar_strain_energy(15): 0.94777 (avg 0.03159)
INFO | accepted=False ATb_norm=2.07e-02 cost_prev=1.2709 cost_new=1.2709
INFO | AL update: snorm=3.4766e-04, csupn=3.4766e-04, max_rho=4.0960e+04
INFO | step #16: cost=0.9478 lambd=0.0000 inexact_tol=1.7e-06
INFO | - augmented_pin_support(2): 0.12436 (avg 0.03109)
INFO | - augmented_prescribed_displacement(1): 0.09748 (avg 0.04874)
INFO | - bar_strain_energy(15): 0.94777 (avg 0.03159)
INFO | accepted=True ATb_norm=1.85e+01 cost_prev=1.1696 cost_new=1.1627
INFO | step #17: cost=1.0021 lambd=0.0000 inexact_tol=1.7e-06
INFO | - augmented_pin_support(2): 0.09877 (avg 0.02469)
INFO | - augmented_prescribed_displacement(1): 0.06189 (avg 0.03094)
INFO | - bar_strain_energy(15): 1.00208 (avg 0.03340)
INFO | accepted=True ATb_norm=1.52e-02 cost_prev=1.1627 cost_new=1.1627
INFO | AL update: snorm=5.4568e-05, csupn=5.4568e-05, max_rho=4.0960e+04
INFO | step #18: cost=1.0021 lambd=0.0000 inexact_tol=6.0e-07
INFO | - augmented_pin_support(2): 0.10889 (avg 0.02722)
INFO | - augmented_prescribed_displacement(1): 0.06537 (avg 0.03269)
INFO | - bar_strain_energy(15): 1.00206 (avg 0.03340)
INFO | accepted=True ATb_norm=3.62e+00 cost_prev=1.1763 cost_new=1.1761
INFO | step #19: cost=1.0125 lambd=0.0000 inexact_tol=6.0e-07
INFO | - augmented_pin_support(2): 0.10107 (avg 0.02527)
INFO | - augmented_prescribed_displacement(1): 0.06252 (avg 0.03126)
INFO | - bar_strain_energy(15): 1.01249 (avg 0.03375)
INFO | accepted=False ATb_norm=1.04e-02 cost_prev=1.1761 cost_new=1.1761
INFO | AL update: snorm=1.3225e-05, csupn=1.3225e-05, max_rho=4.0960e+04
INFO | step #20: cost=1.0125 lambd=0.0000 inexact_tol=6.0e-07
INFO | - augmented_pin_support(2): 0.10341 (avg 0.02585)
INFO | - augmented_prescribed_displacement(1): 0.06317 (avg 0.03158)
INFO | - bar_strain_energy(15): 1.01249 (avg 0.03375)
INFO | accepted=True ATb_norm=8.31e-01 cost_prev=1.1791 cost_new=1.1790
INFO | step #21: cost=1.0147 lambd=0.0000 inexact_tol=6.0e-07
INFO | - augmented_pin_support(2): 0.10162 (avg 0.02541)
INFO | - augmented_prescribed_displacement(1): 0.06267 (avg 0.03133)
INFO | - bar_strain_energy(15): 1.01474 (avg 0.03382)
INFO | accepted=True ATb_norm=1.68e-02 cost_prev=1.1790 cost_new=1.1790
INFO | AL update: snorm=3.1461e-06, csupn=3.1461e-06, max_rho=4.0960e+04
INFO | Terminated @ iteration #22: cost=1.0147 criteria=[0 0 1], term_deltas=4.0e-05,9.9e-03,2.3e-08
Results and visualization#
Node Displacements:
Node dx [mm] dy [mm]
--------------------------
0 -0.003 -0.001
1 -1.153 -11.739
2 0.000 -19.999
3 1.153 -11.739
4 0.003 -0.001
5 4.598 -5.452
6 2.287 -16.315
7 -2.287 -16.315
8 -4.598 -5.452
Member Forces:
Member Force [kN] Type
------------------------------
0- 1 -0.02 Compression
1- 2 0.02 Tension
2- 3 0.02 Tension
3- 4 -0.02 Compression
5- 6 -0.04 Compression
6- 7 -0.08 Compression
7- 8 -0.04 Compression
0- 5 -0.03 Compression
5- 1 0.03 Tension
1- 6 -0.03 Compression
6- 2 0.03 Tension
2- 7 0.03 Tension
7- 3 -0.03 Compression
3- 8 0.03 Tension
8- 4 -0.03 Compression
The solver found the equilibrium configuration of the Warren truss bridge under a prescribed displacement:
Deformed shape: Shown with exaggerated displacements for visibility
Member colors: Red indicates tension, blue indicates compression
Key observations:
Bottom chord is in tension (red) - it resists the spreading of the supports
Top chord is in compression (blue) - it’s being squeezed as the bridge sags
Diagonal members alternate between tension and compression
Maximum deflection occurs at the center where the load is applied
This is the classic behavior of a simply-supported truss bridge under a center point load.
Varying displacements#
We can animate the truss response to different prescribed displacements. As the displacement increases, the internal forces grow proportionally (since we’re in the linear elastic regime).
Using jax.vmap, we solve for all displacement magnitudes in parallel.
INFO | Building optimization problem with 18 terms and 9 variables: 15 costs, 3 eq_zero, 0 leq_zero, 0 geq_zero
INFO | Vectorizing constraint group with 2 constraints (constraint_eq_zero), 1 variables each: augmented_pin_support
INFO | Vectorizing group with 15 costs, 2 variables each: bar_strain_energy
INFO | Vectorizing constraint group with 1 constraints (constraint_eq_zero), 1 variables each: augmented_prescribed_displacement
Solved for 21 displacement values in parallel using vmap