explicit: deformable contact and friction, verified by bars, sliding blocks, the patch test and Hertz

Segment-to-segment penalty contact between element faces: each secondary
face and each main face facing it are projected onto the secondary face's
plane, split into triangles, clipped (Sutherland–Hodgman) and integrated
with a three-point rule per sub-triangle (Puso–Laursen segmentation, the
penalty at the integration points as Fischer–Wriggers), so a uniform
pressure gives the consistent nodal forces on both meshes and the contact
patch test passes on non-matching meshes. Every contact runs both ways at
half the penalty: pairs with each surface as the secondary, general and
self contact face against face. A bucket grid of face boxes every
increment, faces integrated in parallel and their forces added in face
order: the same numbers on any thread count.

The penalty per area ε = s γ min(m/A)/Δt² comes from the lumped masses,
each node's directional area and the stable increment, bounded by
Gershgorin's row sums so contact never shortens the increment. Coulomb
friction with exponential decay by a regularised return map at the
secondary nodes; rigid walls take Coulomb impulses. Contact work and
friction dissipation join the energy balance; each contact reports its
peak force, first and last contact, deepest penetration, friction and the
nodes adjusted at the start (initial penetrations adjusted strain-free or
refused with the node, face and depth); a cap on contact faces.

Verified (crates/explicit/tests/contact.rs, tests/oracle/explicit-*):
colliding bars (2L/c +1.2%, ρcAv₀/2, KE kept to 0.005%, contact energy
back to 8e-5 of it), blocks sliding to a stop on a rigid wall and on a
plate under constant and decaying friction (0.005–0.2%), the patch test
(σ_zz uniform to 0.06%), two spheres colliding against Hertz (peak +2.3%,
duration −0.4%; the oracle case waits for a graded sphere mesher), a free
body untouched by general contact, determinism across threads. Oracle
measures gain `over` (ratios). Existing fixtures do not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 12:49:45 -04:00
1 parent ecf947c090
commit a30d0b5c5b
37 files changed
+2459 -55

No files matched your search

@@ -0,0 +1,9 @@
# Two identical elastic bars colliding (explicit contact)
Two bars 10 × 0.1 × 0.1 of 200 one-point hex8 elements each (E = 10⁴, ρ = 1, ν = 0, c = 100),
end to end and touching at x = 10 with separate nodes; the first moves at v₀ = 0.1 into the
second, at rest. A frictionless contact pair joins the two end faces (each face's nodes kept out
of the other, segment to segment). One-dimensional theory: the bars stay in contact for 2L/c =
0.2 with a force ρcAv₀/2 across the interface, then the striker stops and the struck bar leaves at
v₀ with all the kinetic energy. Linear bulk viscosity is off (it dissipates at the wave fronts).
Solved to t = 0.3.
@@ -0,0 +1,107 @@
{
"title": "Two identical elastic bars colliding (explicit contact)",
"kind": "theory",
"source": "One-dimensional elastic impact of equal bars: contact for 2L/c, interface force ρcAv₀/2, exchange of velocities, no loss of energy; Graff, Wave Motion in Elastic Solids, Dover 1991, §2.1–2.3; Johnson, Impact Strength of Materials, Arnold 1972, §1.6",
"quantities": [
{
"id": "duration",
"quantity": "time of the last contact (the bars touch from t = 0) against 2L/c = 0.2",
"measure": {
"from": "solve",
"pointer": "/contacts/0/last_contact"
},
"value": 0.2,
"tolerance": {
"relative": 0.02
},
"reason": "the penalty springs at the interface delay the release by a few element transit times (about 1% of 2L/c with 200 elements per bar)"
},
{
"id": "force",
"quantity": "the largest force across the interface against ρcAv₀/2 = 0.05",
"measure": {
"from": "solve",
"pointer": "/contacts/0/peak_force"
},
"value": 0.05,
"tolerance": {
"relative": 0.04
},
"reason": "the plateau is ρcAv₀/2 to 0.01%; the peak is the plateau plus the ringing behind the discrete wave front, a few percent"
},
{
"id": "v_a",
"quantity": "velocity of the struck bar's far end after the impact (0: the striker stops)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Impact",
"at": [
[
0,
0,
0
]
]
},
"pointer": "/nodes/0/velocity/0"
},
"value": 0.0,
"tolerance": {
"absolute": 0.005
},
"reason": "the striker stops on average (its kinetic energy is under 0.01% of the start's, see ke); its far end node keeps the ringing of the discrete release front, a few percent of v₀"
},
{
"id": "v_b",
"quantity": "velocity of the struck bar's far end after the impact (v₀ = 0.1)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Impact",
"at": [
[
20.0,
0,
0
]
]
},
"pointer": "/nodes/0/velocity/0"
},
"value": 0.1,
"tolerance": {
"relative": 0.05
},
"reason": "the struck bar leaves at v₀ on average; its far end node keeps the ringing of the discrete wave fronts, a few percent"
},
{
"id": "ke",
"quantity": "kinetic energy after the impact over ½mv₀² (frictionless elastic impact)",
"measure": {
"from": "solve",
"pointer": "/energy/kinetic",
"scale": 2000.0
},
"value": 1.0,
"tolerance": {
"relative": 0.01
},
"reason": "an elastic impact keeps the kinetic energy; the quadratic bulk viscosity at the compression fronts and the vibration left in the bars take a fraction of a percent"
},
{
"id": "contact_energy",
"quantity": "energy left in the contact after the separation, over ½mv₀² (its drift)",
"measure": {
"from": "solve",
"pointer": "/energy/contact",
"scale": 2000.0
},
"value": 0.0,
"tolerance": {
"absolute": 0.001
},
"reason": "the penalty springs give back what they took; the trapezoidal work of a spring that engages between increments drifts by far less than 0.1%"
}
]
}
@@ -0,0 +1,29 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"contact_energy": {
"band": 0.001,
"value": -0.00007743495029686868
},
"duration": {
"band": 2.0242265804764194e-10,
"value": 0.20242265804764192
},
"force": {
"band": 5.1469625943477736e-11,
"value": 0.05146962594347773
},
"ke": {
"band": 1e-9,
"value": 0.9999532471694228
},
"v_a": {
"band": 0.005,
"value": -0.0033987044489733886
},
"v_b": {
"band": 1.0339870444897614e-10,
"value": 0.10339870444897613
}
}
}
@@ -0,0 +1,14 @@
[
{"command": "mesh.block", "params": {"body": "BarA", "kind": "hex8", "corners": [[0, 0, 0], [10.0, 0, 0], [10.0, 0.1, 0], [0, 0.1, 0], [0, 0, 0.1], [10.0, 0, 0.1], [10.0, 0.1, 0.1], [0, 0.1, 0.1]], "divisions": [200, 1, 1], "faces": {"4": "end"}}},
{"command": "mesh.block", "params": {"body": "BarB", "kind": "hex8", "corners": [[10.0, 0, 0], [20.0, 0, 0], [20.0, 0.1, 0], [10.0, 0.1, 0], [10.0, 0, 0.1], [20.0, 0, 0.1], [20.0, 0.1, 0.1], [10.0, 0.1, 0.1]], "divisions": [200, 1, 1], "faces": {"6": "start"}, "merge": 0}},
{"command": "model.set.create", "params": {"name": "A", "geometry": [{"body": "BarA"}]}},
{"command": "model.set.create", "params": {"name": "B", "geometry": [{"body": "BarB"}]}},
{"command": "model.set.create", "params": {"name": "a_end", "geometry": [{"body": "BarA", "face": "end"}]}},
{"command": "model.set.create", "params": {"name": "b_start", "geometry": [{"body": "BarB", "face": "start"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 10000.0, "poisson": 0.0}, "density": 1.0}},
{"command": "model.section.create", "params": {"name": "A", "set": "A", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "model.section.create", "params": {"name": "B", "set": "B", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "interaction.contact.pair", "params": {"name": "impact", "main": "b_start", "secondary": "a_end"}},
{"command": "model.step.create", "params": {"name": "Impact", "procedure": {"type": "explicit_dynamic", "end_time": 0.3, "initial_velocities": [{"set": "A", "velocity": [0.1, 0, 0]}], "bulk_viscosity": {"linear": 0.0, "quadratic": 1.5}}, "boundary_conditions": [], "loads": []}},
{"command": "solve.explicit", "params": {"step": "Impact"}, "id": "solve"}
]
@@ -0,0 +1,9 @@
# Contact patch test (explicit contact, non-matching meshes)
Two unit cubes stacked along z, meshed independently: 3 × 3 × 2 hex8 below, 2 × 2 × 2 above, so
the nodes of the interface at z = 1 do not match. A frictionless contact pair joins the lower
block's top and the upper block's bottom. The faces x = 0 and y = 0 of both blocks are symmetry
planes (normal displacement held), the base z = 0 is held in z, and the top z = 2 is pushed down by
0.01 at constant speed over a hundred wave transits (E = 1000, ν = 0.3, ρ = 1). The exact state
is uniaxial stress, uniform in both blocks; the stresses are compared as ratios, since the penalty
takes a small, uniform share of the squeeze.
@@ -0,0 +1,176 @@
{
"title": "Contact patch test: two blocks with non-matching meshes in uniaxial compression (explicit contact)",
"kind": "theory",
"source": "The contact patch test: a contact discretisation must transmit a uniform pressure across a non-matching interface exactly; Taylor & Papadopoulos, On a patch test for contact problems in two dimensions, Nonlinear Computational Mechanics, Springer 1991, 690–702; Crisfield, Re-visiting the contact patch test, IJNME 48:435–449, 2000. Uniaxial stress between frictionless symmetry planes is uniform in both blocks",
"quantities": [
{
"id": "lower_corner",
"quantity": "σ_zz of the lower block's corner element under the interface (element 10) over that of the upper block's corner element over it (element 19)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
10
]
},
"pointer": "/elements/0/stress/2",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
19
]
},
"pointer": "/elements/0/stress/2"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.005
},
"reason": "a uniform stress is an equilibrium of the discrete system (the integrated contact forces are the consistent nodal forces of a uniform pressure on both meshes); what is left is the ringing of the loading, a hundred wave transits slow"
},
{
"id": "lower_centre",
"quantity": "σ_zz of the lower block's centre element under the interface (element 14) over that of element 19",
"measure": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
14
]
},
"pointer": "/elements/0/stress/2",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
19
]
},
"pointer": "/elements/0/stress/2"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.005
},
"reason": "as for the corner element"
},
{
"id": "lower_base",
"quantity": "σ_zz of the lower block's bottom corner element (element 1) over that of element 19",
"measure": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
1
]
},
"pointer": "/elements/0/stress/2",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
19
]
},
"pointer": "/elements/0/stress/2"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.005
},
"reason": "as for the corner element"
},
{
"id": "upper_top",
"quantity": "σ_zz of the upper block's top far corner element (element 26) over that of element 19",
"measure": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
26
]
},
"pointer": "/elements/0/stress/2",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
19
]
},
"pointer": "/elements/0/stress/2"
}
},
"value": 1.0,
"tolerance": {
"relative": 0.005
},
"reason": "as for the corner element"
},
{
"id": "force",
"quantity": "the force across the interface over −σ_zz A (A = 1)",
"measure": {
"from": "solve",
"pointer": "/contacts/0/peak_force",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
19
]
},
"pointer": "/elements/0/stress/2",
"scale": -1.0
}
},
"value": 1.0,
"tolerance": {
"relative": 0.01
},
"reason": "the integrated contact pressure carries the whole load; the largest force of the run is the final one in a slow squeeze, up to the ringing"
},
{
"id": "lateral",
"quantity": "σ_xx of element 14 over its σ_zz (uniaxial stress: 0)",
"measure": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
14
]
},
"pointer": "/elements/0/stress/0",
"over": {
"command": "solve.probe",
"params": {
"step": "Squeeze",
"elements": [
14
]
},
"pointer": "/elements/0/stress/2"
}
},
"value": 0.0,
"tolerance": {
"absolute": 0.005
},
"reason": "frictionless symmetry planes and a frictionless interface leave the blocks free to expand sideways; the non-matching interface must not hold them"
}
]
}
@@ -0,0 +1,29 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"force": {
"band": 1.0025883998151134e-9,
"value": 1.0025883998151133
},
"lateral": {
"band": 0.005,
"value": -0.00036052361311792505
},
"lower_base": {
"band": 1.00013429984765e-9,
"value": 1.00013429984765
},
"lower_centre": {
"band": 1.0002867148178652e-9,
"value": 1.0002867148178651
},
"lower_corner": {
"band": 1.0001179568787019e-9,
"value": 1.0001179568787018
},
"upper_top": {
"band": 1e-9,
"value": 0.9994358602920412
}
}
}
@@ -0,0 +1,22 @@
[
{"command": "mesh.block", "params": {"body": "Lower", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], "divisions": [3, 3, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}}},
{"command": "mesh.block", "params": {"body": "Upper", "kind": "hex8", "corners": [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2]], "divisions": [2, 2, 2], "faces": {"1": "bottom", "2": "top", "3": "y0", "6": "x0"}, "merge": 0}},
{"command": "model.set.create", "params": {"name": "lower", "geometry": [{"body": "Lower"}]}},
{"command": "model.set.create", "params": {"name": "upper", "geometry": [{"body": "Upper"}]}},
{"command": "model.set.create", "params": {"name": "lower_top", "geometry": [{"body": "Lower", "face": "top"}]}},
{"command": "model.set.create", "params": {"name": "upper_bottom", "geometry": [{"body": "Upper", "face": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "x0", "geometry": [{"body": "Lower", "face": "x0"}, {"body": "Upper", "face": "x0"}]}},
{"command": "model.set.create", "params": {"name": "y0", "geometry": [{"body": "Lower", "face": "y0"}, {"body": "Upper", "face": "y0"}]}},
{"command": "model.set.create", "params": {"name": "z0", "geometry": [{"body": "Lower", "face": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Upper", "face": "top"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000.0, "poisson": 0.3}, "density": 1.0}},
{"command": "model.section.create", "params": {"name": "lower", "set": "lower", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "model.section.create", "params": {"name": "upper", "set": "upper", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "interaction.contact.pair", "params": {"name": "interface", "main": "lower_top", "secondary": "upper_bottom"}},
{"command": "model.boundary_condition.create", "params": {"name": "x0", "type": "displacement", "target": {"set": "x0"}, "values": [0, null, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "y0", "type": "displacement", "target": {"set": "y0"}, "values": [null, 0, null, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "z0", "type": "displacement", "target": {"set": "z0"}, "values": [null, null, 0, null, null, null]}},
{"command": "model.boundary_condition.create", "params": {"name": "push", "type": "displacement", "target": {"set": "top"}, "values": [null, null, -0.01, null, null, null]}},
{"command": "model.step.create", "params": {"name": "Squeeze", "procedure": {"type": "explicit_dynamic", "end_time": 5.451}, "boundary_conditions": ["x0", "y0", "z0", "push"], "loads": []}},
{"command": "solve.explicit", "params": {"step": "Squeeze"}, "id": "solve"}
]
@@ -0,0 +1,8 @@
# Two elastic spheres colliding head-on (explicit contact, Hertz)
Two identical steel spheres, R = 10 mm, E = 210 GPa, ν = 0.3, ρ = 7.85 t/m³ (mm, t, s, N, MPa),
meet head-on at a relative speed of 1 m/s. Hertz's quasi-static theory gives the approach
δ = 0.020441 mm, the contact radius a = √(R* δ) = 0.320 mm, the peak force 1005.4 N and the
contact duration 60.16 μs. The case waits for a graded sphere mesh built from commands: the contact
radius is a thirtieth of the sphere's, so the mesh must be fine at the poles and coarse elsewhere.
The explicit crate's tests check the same theory on a cubed-sphere mesh generated in code.
@@ -0,0 +1,11 @@
{
"title": "Two elastic spheres in a head-on collision: Hertz's force and contact duration (explicit contact)",
"kind": "theory",
"source": "Hertz's quasi-static impact: with R* = R/2, m* = m/2, E* = E/(2(1 − ν²)), the approach reaches δ = (15 m* v²/(16 E* √R*))^(2/5), the force (4/3) E* √R* δ^(3/2), and the contact lasts 2.9432 δ/v; Hertz, J. reine angew. Math. 92:156–171, 1882; K. L. Johnson, Contact Mechanics, Cambridge 1985, §11.4",
"status": "pending",
"requires": "a graded sphere mesh from commands (a cubed-sphere hex mesher, or tet refinement towards a point); until then the explicit crate's tests `spheres_collide_as_hertz_says` (a graded cubed-sphere quarter of each sphere, 27 648 hex8: peak force +2.3%, duration −0.4%) and its coarse twin in the main gate check this case on a mesh generated in code",
"quantities": [
{"id": "force", "quantity": "the largest contact force, two steel spheres R = 10 mm (E = 210 GPa, ν = 0.3, ρ = 7.85 t/m³) meeting at 1 m/s (N)", "value": 1005.38},
{"id": "duration", "quantity": "the contact duration (s)", "value": 6.0163e-5}
]
}
@@ -0,0 +1,8 @@
# Block sliding to a stop on a deformable plate (explicit contact, decaying friction)
The block of `explicit-sliding-block-wall` (1 × 1 × 0.5, 4 × 4 × 2 hex8, E = 10⁵, ν = 0.3,
ρ = 1) rests on a plate 4.5 × 2 × 0.5 of 9 × 4 × 1 hex8 held at its base, the two meshes not
matching; a contact pair joins the plate's top and the block's bottom with Coulomb friction whose
coefficient falls with the slip rate, μ = 0.2 + 0.3 e^(−1.5 v). Under gravity g = 10 the block
is launched along x at v₀ = 3. A rigid block decelerates at g μ(v), stops after 1.2001 and travels
∫₀^v₀ v dv / (g μ(v)) = 2.01524. Solved to t = 1.56.
@@ -0,0 +1,15 @@
{
"title": "Block sliding to a stop on a deformable plate, friction decaying with the slip rate (explicit contact)",
"kind": "theory",
"source": "A rigid block launched at v₀ with friction μ(v) = μk + (μs − μk) e^(−d v) decelerates at g μ(v); it travels ∫₀^v₀ v dv / (g μ(v)) (Simpson's rule, 200 000 intervals) and stops after [v₀ + ln((μk + (μs − μk) e^(−d v₀))/μs)/d]/(g μk) = 1.2001; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5; the exponential decay law as in Oden & Martins, CMAME 52:527–634, 1985",
"quantities": [
{"id": "distance", "quantity": "distance travelled (top centre node) against ∫ v dv / (g μ(v)) = 2.01524",
"measure": {"command": "solve.probe", "params": {"step": "Slide", "at": [[0.5, 0.5, 0.5]]}, "pointer": "/nodes/0/displacement/0"},
"value": 2.015240387808855, "tolerance": {"relative": 0.01},
"reason": "the block bounces on the penalty springs and the plate under it shears elastically, so the normal force and the slip rate it feels vary by a little around their rigid-body values"},
{"id": "velocity", "quantity": "velocity of the top centre node after the stop",
"measure": {"command": "solve.probe", "params": {"step": "Slide", "at": [[0.5, 0.5, 0.5]]}, "pointer": "/nodes/0/velocity/0"},
"value": 0.0, "tolerance": {"absolute": 0.03},
"reason": "the block sticks; what is left is its elastic vibration, under 1% of v₀"}
]
}
@@ -0,0 +1,13 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"distance": {
"band": 2.0152403878088553e-9,
"value": 2.011776866282693
},
"velocity": {
"band": 0.03,
"value": -0.00003285207146964345
}
}
}
@@ -0,0 +1,17 @@
[
{"command": "mesh.block", "params": {"body": "Block", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0.5], [1, 0, 0.5], [1, 1, 0.5], [0, 1, 0.5]], "divisions": [4, 4, 2], "faces": {"1": "bottom"}}},
{"command": "mesh.block", "params": {"body": "Plate", "kind": "hex8", "corners": [[-0.5, -0.5, -0.5], [4.0, -0.5, -0.5], [4.0, 1.5, -0.5], [-0.5, 1.5, -0.5], [-0.5, -0.5, 0], [4.0, -0.5, 0], [4.0, 1.5, 0], [-0.5, 1.5, 0]], "divisions": [9, 4, 1], "faces": {"1": "base", "2": "top"}, "merge": 0}},
{"command": "model.set.create", "params": {"name": "block", "geometry": [{"body": "Block"}]}},
{"command": "model.set.create", "params": {"name": "plate", "geometry": [{"body": "Plate"}]}},
{"command": "model.set.create", "params": {"name": "bottom", "geometry": [{"body": "Block", "face": "bottom"}]}},
{"command": "model.set.create", "params": {"name": "top", "geometry": [{"body": "Plate", "face": "top"}]}},
{"command": "model.set.create", "params": {"name": "base", "geometry": [{"body": "Plate", "face": "base"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 100000.0, "poisson": 0.3}, "density": 1.0}},
{"command": "model.section.create", "params": {"name": "block", "set": "block", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "model.section.create", "params": {"name": "plate", "set": "plate", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "interaction.contact.pair", "params": {"name": "slide", "main": "top", "secondary": "bottom", "friction": {"static": 0.5, "kinetic": 0.2, "decay": 1.5}}},
{"command": "model.boundary_condition.create", "params": {"name": "hold", "type": "fixed", "target": {"set": "base"}}},
{"command": "model.load.create", "params": {"name": "weight", "type": "gravity", "acceleration": [0, 0, -10.0], "set": "block"}},
{"command": "model.step.create", "params": {"name": "Slide", "procedure": {"type": "explicit_dynamic", "end_time": 1.56, "initial_velocities": [{"set": "block", "velocity": [3.0, 0, 0]}]}, "boundary_conditions": ["hold"], "loads": ["weight"]}},
{"command": "solve.explicit", "params": {"step": "Slide"}, "id": "solve"}
]
@@ -0,0 +1,7 @@
# Block sliding to a stop on a rigid wall (explicit, Coulomb friction)
A block 1 × 1 × 0.5 of 4 × 4 × 2 one-point hex8 elements (E = 10⁵, ν = 0.3, ρ = 1, so m = 0.5)
rests on a rigid wall at z = 0 (its bottom nodes) with Coulomb friction μ = 0.3, under gravity
g = 10, and is launched along x at v₀ = 3. Theory for a rigid block: deceleration μg, stop after
v₀/(μg) = 1 at v₀²/(2μg) = 1.5, the kinetic energy ½mv₀² = 2.25 all dissipated by friction.
Solved to t = 1.3.
@@ -0,0 +1,19 @@
{
"title": "Block sliding to a stop on a rigid wall with Coulomb friction (explicit)",
"kind": "theory",
"source": "A rigid block launched at v₀ on a horizontal plane with Coulomb friction μ decelerates at μg and stops after v₀/(μg) at v₀²/(2μg), its kinetic energy all dissipated by friction; Meriam & Kraige, Engineering Mechanics: Dynamics, §3/5 (work and energy)",
"quantities": [
{"id": "distance", "quantity": "distance travelled (top centre node) against v₀²/(2μg) = 1.5",
"measure": {"command": "solve.probe", "params": {"step": "Slide", "at": [[0.5, 0.5, 0.5]]}, "pointer": "/nodes/0/displacement/0"},
"value": 1.5, "tolerance": {"relative": 0.01},
"reason": "the wall's friction impulses are exact Coulomb impulses; the block's elastic vibration from the sudden gravity and the friction moment move the node by a fraction of a percent"},
{"id": "velocity", "quantity": "velocity of the top centre node after the stop",
"measure": {"command": "solve.probe", "params": {"step": "Slide", "at": [[0.5, 0.5, 0.5]]}, "pointer": "/nodes/0/velocity/0"},
"value": 0.0, "tolerance": {"absolute": 0.03},
"reason": "the block sticks; what is left is its elastic vibration, under 1% of v₀"},
{"id": "dissipated", "quantity": "friction dissipation over ½mv₀² = 2.25",
"measure": {"from": "solve", "pointer": "/energy/friction", "scale": 0.4444444444444444},
"value": 1.0, "tolerance": {"relative": 0.01},
"reason": "the kinetic energy goes into friction; the vibration and the work of gravity as the block settles add or take a fraction of a percent"}
]
}
@@ -0,0 +1,17 @@
{
"note": "FieldCraft's accepted results for this case, written by `cargo xtask oracle --bless`. A result outside its band fails CI.",
"values": {
"dissipated": {
"band": 1.0012078586619573e-9,
"value": 1.0012078586619573
},
"distance": {
"band": 1.5000000000000002e-9,
"value": 1.4999205464422256
},
"velocity": {
"band": 0.03,
"value": 0.00027058229054461555
}
}
}
@@ -0,0 +1,10 @@
[
{"command": "mesh.block", "params": {"body": "Block", "kind": "hex8", "corners": [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 0.5], [1, 0, 0.5], [1, 1, 0.5], [0, 1, 0.5]], "divisions": [4, 4, 2], "faces": {"1": "bottom"}}},
{"command": "model.set.create", "params": {"name": "block", "geometry": [{"body": "Block"}]}},
{"command": "model.set.create", "params": {"name": "bottom", "geometry": [{"body": "Block", "face": "bottom"}]}},
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 100000.0, "poisson": 0.3}, "density": 1.0}},
{"command": "model.section.create", "params": {"name": "block", "set": "block", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
{"command": "model.load.create", "params": {"name": "weight", "type": "gravity", "acceleration": [0, 0, -10.0], "set": "block"}},
{"command": "model.step.create", "params": {"name": "Slide", "procedure": {"type": "explicit_dynamic", "end_time": 1.3, "initial_velocities": [{"set": "block", "velocity": [3.0, 0, 0]}], "rigid_walls": [{"set": "bottom", "point": [0, 0, 0], "normal": [0, 0, 1], "friction": {"static": 0.3}}]}, "boundary_conditions": [], "loads": ["weight"]}},
{"command": "solve.explicit", "params": {"step": "Slide"}, "id": "solve"}
]