# =============================================================================
# Stress on Truss
# =============================================================================
# Source: https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/stress_on_truss/
#
# 2D truss structure: point masses connected by stiff springs (beams).
# Demonstrates:
# - Heterogeneous vertices: FreeVertex (4 SVs) + FixedVertex (0 SVs)
# - 2D coupling with insym/outsym
# - Observed function (obsf) for beam force magnitude
# - Per-edge parameters (beam rest lengths L)
# =============================================================================
id: 106
label: "Stress on Truss"
description: >
Time evolution of a 2D truss structure: point masses connected by
stiff springs. Free vertices have position (x, y) and velocity (vx, vy)
as state variables; fixed vertices output their constant position.
Beams exert forces proportional to deviation from rest length.
Recreates the NetworkDynamics.jl Stress on Truss tutorial.
references:
- "https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/stress_on_truss/"
# -- Default vertex dynamics (FreeVertex) --------------------------------------
dynamics:
name: FreeVertex
label: "Free vertex (point mass)"
description: >
Point mass with position (x, y) and velocity (vx, vy).
dx/dt = vx, dy/dt = vy,
dvx/dt = (Fx_sum - γ·vx) / M,
dvy/dt = (Fy_sum - γ·vy) / M - g.
system_type: continuous
autonomous: true
parameters:
M:
label: "Mass"
symbol: "M"
value: 10.0
unit: "kg"
description: "Point mass"
gamma:
label: "Damping"
symbol: "γ"
value: 200.0
unit: "kg/s"
description: "Velocity damping coefficient"
g:
label: "Gravitational acceleration"
symbol: "g"
value: 9.81
unit: "m/s²"
description: "Gravitational acceleration"
coupling_terms:
c_Fx:
description: "Sum of horizontal beam forces"
c_Fy:
description: "Sum of vertical beam forces"
state_variables:
vx:
label: "Horizontal velocity"
symbol: "vx"
equation:
rhs: "(c_Fx - gamma * vx) / M"
initial_value: 0.0
unit: "m/s"
variable_of_interest: true
vy:
label: "Vertical velocity"
symbol: "vy"
equation:
rhs: "(c_Fy - gamma * vy) / M - g"
initial_value: 0.0
unit: "m/s"
variable_of_interest: true
x:
label: "Horizontal position"
symbol: "x"
equation:
rhs: "vx"
initial_value: 0.0
unit: "m"
coupling_variable: true
variable_of_interest: true
y:
label: "Vertical position"
symbol: "y"
equation:
rhs: "vy"
initial_value: 0.0
unit: "m"
coupling_variable: true
variable_of_interest: true
# -- Network -------------------------------------------------------------------
network:
dynamics:
FixedVertex:
name: FixedVertex
label: "Fixed vertex (support)"
description: >
Fixed support point. No internal dynamics, outputs constant
position (xfix, yfix) as coupling variables.
Uses NoFeedForward since it ignores edge inputs.
system_type: continuous
autonomous: true
parameters:
xfix:
label: "Fixed x position"
symbol: "xfix"
value: 0.0
unit: "m"
yfix:
label: "Fixed y position"
symbol: "yfix"
value: 0.0
unit: "m"
coupling_terms: {}
state_variables: {}
label: "11-node truss structure"
description: >
2D truss: 5 bottom nodes, 5 top nodes (shifted), 1 hanging mass.
Bottom: (0,0), (1,0), (2,0), (3,0), (4,0).
Top: (1.2,1), (2.2,1), (3.2,1), (4.2,1), (5.2,1).
Hanging: (6,−1). Nodes 1 and 4 are fixed supports.
18 beams connecting adjacent nodes.
number_of_nodes: 11
nodes:
# Bottom row (nodes 1-5, indices 0-4)
- id: 0
label: "Bottom-left support"
dynamics: FixedVertex
parameters:
- name: xfix
value: 0.0
- name: yfix
value: 0.0
- id: 1
label: "Bottom 2"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 1.0
y:
value: 0.0
- id: 2
label: "Bottom 3"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 2.0
y:
value: 0.0
- id: 3
label: "Bottom-right support"
dynamics: FixedVertex
parameters:
- name: xfix
value: 3.0
- name: yfix
value: 0.0
- id: 4
label: "Bottom 5"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 4.0
y:
value: 0.0
# Top row (nodes 6-10, indices 5-9)
- id: 5
label: "Top 1"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 1.2
y:
value: 1.0
- id: 6
label: "Top 2"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 2.2
y:
value: 1.0
- id: 7
label: "Top 3"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 3.2
y:
value: 1.0
- id: 8
label: "Top 4"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 4.2
y:
value: 1.0
- id: 9
label: "Top 5"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 5.2
y:
value: 1.0
# Hanging mass (node 11, index 10)
- id: 10
label: "Hanging mass"
state:
vx:
value: 0.0
vy:
value: 0.0
x:
value: 6.0
y:
value: -1.0
parameters:
- name: M
value: 200.0
- name: gamma
value: 100.0
edges:
# Vertical beams: bottom i ↔ top i+N (i=1..5)
- source: 0
target: 5
parameters:
- name: L
value: 1.562050
- source: 1
target: 6
parameters:
- name: L
value: 1.562050
- source: 2
target: 7
parameters:
- name: L
value: 1.562050
- source: 3
target: 8
parameters:
- name: L
value: 1.562050
- source: 4
target: 9
parameters:
- name: L
value: 1.562050
# Cross braces: bottom i+1 ↔ top i (i=1..4)
- source: 1
target: 5
parameters:
- name: L
value: 1.019804
- source: 2
target: 6
parameters:
- name: L
value: 1.019804
- source: 3
target: 7
parameters:
- name: L
value: 1.019804
- source: 4
target: 8
parameters:
- name: L
value: 1.019804
# Bottom horizontals: bottom i ↔ bottom i+1 (i=1..4)
- source: 0
target: 1
parameters:
- name: L
value: 1.0
- source: 1
target: 2
parameters:
- name: L
value: 1.0
- source: 2
target: 3
parameters:
- name: L
value: 1.0
- source: 3
target: 4
parameters:
- name: L
value: 1.0
# Top horizontals: top i ↔ top i+1 (i=1..4)
- source: 5
target: 6
parameters:
- name: L
value: 1.0
- source: 6
target: 7
parameters:
- name: L
value: 1.0
- source: 7
target: 8
parameters:
- name: L
value: 1.0
- source: 8
target: 9
parameters:
- name: L
value: 1.0
# Hanging beam: top 5 (index 9) ↔ hanging mass (index 10)
- source: 9
target: 10
parameters:
- name: L
value: 2.154066
coupling:
BeamForce:
name: BeamForce
label: "Stiff spring (beam)"
description: >
Antisymmetric beam force: F = K·(L - d) in the direction
of the beam. d = |pos_dst - pos_src|.
Outputs 2D force vector [Fx, Fy].
Has an observed function for absolute force |F|.
delayed: false
parameters:
K:
label: "Spring stiffness"
value: 0.5e6
unit: "N/m"
description: "Beam stiffness constant"
L:
label: "Rest length"
value: 1.0
unit: "m"
description: "Nominal beam length (set per-edge)"
pre_expression:
rhs: |
dx = v_dst[1] - v_src[1]
dy = v_dst[2] - v_src[2]
d = sqrt(dx^2 + dy^2)
Fabs = K * (L - d)
e_dst[1] = Fabs * dx / d
e_dst[2] = Fabs * dy / d
description: "2D beam force: K·(L - d)·[dx, dy]/d"
outsym:
- Fx
- Fy
observed:
- name: Fabs
label: "Absolute beam force"
description: "Magnitude of beam force: K·(L - d)"
equation:
rhs: |
dx = v_dst[1] - v_src[1]
dy = v_dst[2] - v_src[2]
d = sqrt(dx^2 + dy^2)
obsout[1] = K * (L - d)
# -- Integration ---------------------------------------------------------------
integration:
description: "Explicit Runge-Kutta (Tsit5) with dt=0.01 over t in [0, 12]"
method: Tsit5
step_size: 0.01
duration: 12.0
time_scale: "s"
Stress on Truss
2D truss structure with stiff springs and observed forces
Replicates the NetworkDynamics.jl Stress on Truss tutorial — a 2D truss of point masses connected by stiff beams. Free vertices move under gravity and spring forces; fixed vertices act as supports. Demonstrates heterogeneous vertex types, 2D coupling (insym/outsym), observed functions (obsf), and per-edge parameters.
YAML Specification
Model Report
Stress on Truss
Time evolution of a 2D truss structure: point masses connected by stiff springs. Free vertices have position (x, y) and velocity (vx, vy) as state variables; fixed vertices output their constant position. Beams exert forces proportional to deviation from rest length. Recreates the NetworkDynamics.jl Stress on Truss tutorial.
1. Brain Network: 11-node truss structure
2D truss: 5 bottom nodes, 5 top nodes (shifted), 1 hanging mass. Bottom: (0,0), (1,0), (2,0), (3,0), (4,0). Top: (1.2,1), (2.2,1), (3.2,1), (4.2,1), (5.2,1). Hanging: (6,−1). Nodes 1 and 4 are fixed supports. 18 beams connecting adjacent nodes.
- Regions: 11
Coupling: Stiff spring (beam)
Antisymmetric beam force: F = K·(L - d) in the direction of the beam. d = |pos_dst - pos_src|. Outputs 2D force vector [Fx, Fy]. Has an observed function for absolute force |F|.
Receives \(x\), \(y\) from connected regions.
Pre-synaptic: \(c_{\text{pre}} = dx = v_dst[1] - v_src[1] dy = v_dst[2] - v_src[2] d = sqrt(dx^2 + dy^2) Fabs = K * (L - d) e_dst[1] = Fabs * dx / d e_dst[2] = Fabs * dy / d\)
| Parameter | Value | Description |
|---|---|---|
| \(K\) | 500000.0 | Beam stiffness constant |
| \(L\) | 1.0 | Nominal beam length (set per-edge) |
2. Local Dynamics: Free vertex (point mass)
Point mass with position (x, y) and velocity (vx, vy). The model comprises 4 state variables.
2.1 State Equations
\[\dot{vx} = \frac{c_{Fx} - \gamma \cdot vx}{M}\]
\[\dot{vy} = - g + \frac{c_{Fy} - \gamma \cdot vy}{M}\]
\[\dot{x} = vx\]
\[\dot{y} = vy\]
2.2 Parameters
| Parameter | Value | Unit | Description |
|---|---|---|---|
| \(M\) | 10.0 | kg | Point mass |
| \(g\) | 9.81 | m_per_s2 | Gravitational acceleration |
| \(\gamma\) | 200.0 | kg_per_s | Velocity damping coefficient |
3. Numerical Integration
- Method: Tsit5
- Time step: \(\Delta t = 0.01\) ms
- Duration: 12.0 ms
References
- https://juliadynamics.github.io/NetworkDynamics.jl/stable/generated/stress_on_truss/
Generated Julia Code
Code(exp.render_code("networkdynamics"), language='julia')using Graphs
using NetworkDynamics
using OrdinaryDiffEqTsit5
function FreeVertex_f!(dx, _x, esum, (M, g, gamma,), t)
vx, vy, x, y = _x
c_Fx = esum[1]
c_Fy = esum[2]
dx[1] = (c_Fx - gamma .* vx) ./ M
dx[2] = -g + (c_Fy - gamma .* vy) ./ M
dx[3] = vx
dx[4] = vy
nothing
end
vertex_FreeVertex = VertexModel(;
f = FreeVertex_f!,
g = StateMask(3:4),
sym = [:vx, :vy, :x, :y],
psym = [:M => 10.0, :g => 9.81, :gamma => 200.0],
insym = [:Fx, :Fy],
name = :FreeVertex,
)
function FixedVertex_g!(out, x, p, t)
out .= p
nothing
end
vertex_FixedVertex = VertexModel(;
g = FixedVertex_g!,
outsym = [:x, :y],
psym = [:xfix, :yfix],
ff = NoFeedForward(),
name = :FixedVertex,
)
function BeamForce_edge_g!(e_dst, v_src, v_dst, (K, L,), t)
dx = v_dst[1] - v_src[1]
dy = v_dst[2] - v_src[2]
d = sqrt(dx^2 + dy^2)
Fabs = K * (L - d)
e_dst[1] = Fabs * dx / d
e_dst[2] = Fabs * dy / d
nothing
end
function BeamForce_obsf!(obsout, u, v_src, v_dst, (K, L,), t)
dx = v_dst[1] - v_src[1]
dy = v_dst[2] - v_src[2]
d = sqrt(dx^2 + dy^2)
obsout[1] = K * (L - d)
nothing
end
edge_BeamForce = EdgeModel(;
g = AntiSymmetric(BeamForce_edge_g!),
outsym = [:Fx, :Fy],
psym = [:K => 500000.0, :L => 1.0],
obsf = BeamForce_obsf!,
obssym = [:Fabs],
name = :BeamForce,
)
g = SimpleGraph(11)
add_edge!(g, 1, 6)
add_edge!(g, 2, 7)
add_edge!(g, 3, 8)
add_edge!(g, 4, 9)
add_edge!(g, 5, 10)
add_edge!(g, 2, 6)
add_edge!(g, 3, 7)
add_edge!(g, 4, 8)
add_edge!(g, 5, 9)
add_edge!(g, 1, 2)
add_edge!(g, 2, 3)
add_edge!(g, 3, 4)
add_edge!(g, 4, 5)
add_edge!(g, 6, 7)
add_edge!(g, 7, 8)
add_edge!(g, 8, 9)
add_edge!(g, 9, 10)
add_edge!(g, 10, 11)
vertex_array = VertexModel[vertex_FreeVertex for _ in 1:11]
vertex_array[1] = vertex_FixedVertex
vertex_array[4] = vertex_FixedVertex
nw = Network(g, vertex_array, edge_BeamForce; dealias=true)
s = NWState(nw)
s.p.v[1, :xfix] = 0.0
s.p.v[1, :yfix] = 0.0
s.v[2, :vx] = 0.0
s.v[2, :vy] = 0.0
s.v[2, :x] = 1.0
s.v[2, :y] = 0.0
s.v[3, :vx] = 0.0
s.v[3, :vy] = 0.0
s.v[3, :x] = 2.0
s.v[3, :y] = 0.0
s.p.v[4, :xfix] = 3.0
s.p.v[4, :yfix] = 0.0
s.v[5, :vx] = 0.0
s.v[5, :vy] = 0.0
s.v[5, :x] = 4.0
s.v[5, :y] = 0.0
s.v[6, :vx] = 0.0
s.v[6, :vy] = 0.0
s.v[6, :x] = 1.2
s.v[6, :y] = 1.0
s.v[7, :vx] = 0.0
s.v[7, :vy] = 0.0
s.v[7, :x] = 2.2
s.v[7, :y] = 1.0
s.v[8, :vx] = 0.0
s.v[8, :vy] = 0.0
s.v[8, :x] = 3.2
s.v[8, :y] = 1.0
s.v[9, :vx] = 0.0
s.v[9, :vy] = 0.0
s.v[9, :x] = 4.2
s.v[9, :y] = 1.0
s.v[10, :vx] = 0.0
s.v[10, :vy] = 0.0
s.v[10, :x] = 5.2
s.v[10, :y] = 1.0
s.v[11, :vx] = 0.0
s.v[11, :vy] = 0.0
s.v[11, :x] = 6.0
s.v[11, :y] = -1.0
s.p.v[11, :M] = 200.0
s.p.v[11, :gamma] = 100.0
s.p.e[1, :L] = 1.0
s.p.e[2, :L] = 1.56205
s.p.e[3, :L] = 1.0
s.p.e[4, :L] = 1.019804
s.p.e[5, :L] = 1.56205
s.p.e[6, :L] = 1.0
s.p.e[7, :L] = 1.019804
s.p.e[8, :L] = 1.56205
s.p.e[9, :L] = 1.0
s.p.e[10, :L] = 1.019804
s.p.e[11, :L] = 1.56205
s.p.e[12, :L] = 1.019804
s.p.e[13, :L] = 1.56205
s.p.e[14, :L] = 1.0
s.p.e[15, :L] = 1.0
s.p.e[16, :L] = 1.0
s.p.e[17, :L] = 1.0
s.p.e[18, :L] = 2.154066
tspan = (0.0, 12.0)
prob = ODEProblem(nw, uflat(s), tspan, pflat(s))
sol = solve(prob, Tsit5(); saveat=0.01)
adj_matrix = Float64.(adjacency_matrix(g))
using Random: MersenneTwister
function spring_layout(g; seed=42, iterations=50, k=1.0)
rng = MersenneTwister(seed)
n = nv(g)
pos = randn(rng, n, 2)
for _ in 1:iterations
disp = zeros(n, 2)
for i in 1:n, j in (i+1):n
d = pos[i, :] - pos[j, :]
dist = max(norm(d), 0.01)
rep = k^2 / dist
disp[i, :] .+= d / dist * rep
disp[j, :] .-= d / dist * rep
end
for e in edges(g)
i, j = src(e), dst(e)
d = pos[j, :] - pos[i, :]
dist = max(norm(d), 0.01)
att = dist^2 / k
disp[i, :] .+= d / dist * att
disp[j, :] .-= d / dist * att
end
for i in 1:n
dl = max(norm(disp[i, :]), 0.01)
pos[i, :] .+= disp[i, :] / dl * min(dl, 0.1)
end
end
return pos
end
using LinearAlgebra: norm
node_positions = spring_layout(g)
using Plots
plot(sol; ylabel="state", xlabel="time", title="FreeVertex on 11-node network")
Run & Animate
ts = exp.run(format="networkdynamics")Detected IPython. Loading juliacall extension. See https://juliapy.github.io/PythonCall.jl/stable/compat/#IPython
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.collections import LineCollection
from IPython.display import HTML
# All spatial data is extracted automatically from the YAML metadata
x_all = ts.node_positions[:, :, 0] # (n_t, n_nodes)
y_all = ts.node_positions[:, :, 1] # (n_t, n_nodes)
n_t = len(ts.time)
n_nodes = x_all.shape[1]
# Edge list from experiment metadata
edges = [
(e.source, e.target)
for e in sorted(
exp.network.edges,
key=lambda e: (min(e.source, e.target), max(e.source, e.target)),
)
]
# Edge force magnitudes (extracted automatically from observed functions)
has_fabs = "Fabs" in ts.edge_data
if has_fabs:
fabs = ts.edge_data["Fabs"]
fmax = np.max(np.abs(fabs)) * 0.3
# Node styling from metadata
fixed = ts.fixed_nodes
sizes = [
80 if n in fixed else (400 if ts.node_metadata[n]["params"].get("M") else 40)
for n in range(n_nodes)
]
colors_n = [
(
"red"
if n in fixed
else ("black" if ts.node_metadata[n]["params"].get("M") else "steelblue")
)
for n in range(n_nodes)
]
# Create animation
fig, ax = plt.subplots(figsize=(10, 5), layout="compressed")
if has_fabs:
norm = plt.Normalize(-fmax, fmax)
cmap = plt.get_cmap("coolwarm")
sm = plt.cm.ScalarMappable(norm=norm, cmap=cmap)
fig.colorbar(sm, ax=ax, label="|F|")
def draw_frame(frame_idx):
ax.clear()
xi, yi = x_all[frame_idx], y_all[frame_idx]
# Draw edges colored by force
segments = [[(xi[s], yi[s]), (xi[t], yi[t])] for s, t in edges]
if has_fabs:
lc = LineCollection(segments, linewidths=3, cmap=cmap, norm=norm)
lc.set_array(fabs[frame_idx])
else:
lc = LineCollection(segments, linewidths=3, colors="steelblue")
ax.add_collection(lc)
# Draw nodes
ax.scatter(
xi, yi, s=sizes, c=colors_n, zorder=5, edgecolors="black", linewidths=0.5
)
for n in fixed:
ax.plot(xi[n], yi[n] - 0.08, "^", color="red", markersize=12, zorder=6)
ax.set_xlim(-0.5, 7.0)
ax.set_ylim(-2.5, 1.5)
ax.set_aspect("equal")
ax.set_title(f"Stress on Truss (t = {ts.time[frame_idx]:.2f} s)", fontsize=14)
ax.grid(True, alpha=0.2)
# Subsample frames for animation
n_frames = min(200, n_t)
frame_indices = np.linspace(0, n_t - 1, n_frames, dtype=int)
anim = FuncAnimation(
fig,
lambda i: draw_frame(frame_indices[i]),
frames=n_frames,
interval=50,
blit=False,
)
plt.close(fig)
anim.save("_output/stress_on_truss.gif", writer="pillow", fps=20)