Summary
The manual defines RF as the sum of the external forces in a node, i.e. the loads plus the reaction forces (section 6.11.5 (Forces obtained by selecting RF) of the 2.23 manual, and section 7.99, *NODE PRINT).
At nodes connected by MPCs, ccx decides by element membership what RF shows. Commit 24334f7f (30 Apr 2025, “worked on the reaction force of nodes belonging to MPC’s: resultsforc.”) made resultsforc.c print the element forces alone at a node that belongs to an element, and the force its MPCs carry at a node that belongs to no element. With it came the tests beammrlinx_same.inp and beammrlinx_diff.inp (RF at MEANROT and distributing-coupling nodes outside the structure), and pret4.dat.ref changed (RF of the pre-tension node 161: 1.396597 to -10). That rule is deliberate, and this report does not question it for those nodes. It reports three side effects at supports, where RF should contain the reaction and does not (exit 0, no message):
- A support that belongs to an element and carries a load only through
*EQUATIONs prints RF = 0, while the applied load is 4. - The REF NODE of a
*RIGID BODYprints the body’s reaction (4.0), but 2.2E-16 once aSPRING1element is attached to it, although the held node keeps that spring at zero force. The spring makes it a node that “belongs to an element”. - The same REF NODE prints 0 once it has a
*TRANSFORM, even one whose axes are the global axes.
In each case the printed reactions no longer balance the applied loads, and inputs that mean the same thing (an extra held node, an inert spring, an identity transform) change the printed reaction from 4 to 0.
The manual documents two special cases of this: section 7.113 (*RIGID BODY) says that RF is not correct if the reference node belongs to the rigid body, and section 7.80 (*KINEMATIC) says that RF does not work for the reference node of a kinematic coupling. The cases above are outside both: in (2) and (3) the reference node is not a node of the body, and in (1) there is no rigid body or coupling at all.
Version
ccx 2.23 (macOS arm64). Source: master 9ba5d76 (16 Oct 2026).
Already reported or fixed?
Not reported: I found no matching GitHub issue (open or closed) or pull request in Dhondtguido/CalculiX, and no calculix.discourse.group topic (searched 21 Oct 2026). Not fixed: the logic of resultsforc.c is unchanged on master (9ba5d76); commit c8153110 (12 Jul 2026) only removed commented-out code and re-indented it.
Related:
- How to print the section moment (SOM) at the section on a 1D beam (B32)? - #5 by Disla (Dec 2024, before 24334f7f) mentions “some bugs reported for Reaction Forces on nodes belonging to shell and beam elements” (nodes that ccx connects to their expanded nodes by MPCs); it names no case or cause.
- Technical Proposal: Implementation of Independent Reaction Force Output in CalculiX · Issue #146 · Dhondtguido/CalculiX · GitHub, Technical Proposal: Implementation of Independent Reaction Force Output and summary of applied and reaction forces in CalculiX · Issue #224 · Dhondtguido/CalculiX · GitHub, PR Output only reaction forces. by g0mb4 · Pull Request #154 · Dhondtguido/CalculiX · GitHub and Output reaction forces only ask for (and prototype) a reaction-only output separate from the loads; none of them reports forces missing at nodes that MPCs connect.
- Unexpected Reaction Force on the REF Node of a Pretension Section. · Issue #147 · Dhondtguido/CalculiX · GitHub (RF at the reference node of a pre-tension section) was fixed in 2.23.
- Zero reaction moment when reading from ROT NODE in 2D reports a zero reaction moment at a ROT NODE in a plane-stress gear model with contact; a plain plane-stress rigid body without contact prints the right moment in 2.23, so this report does not cover that case.
Minimal input
rf_eq.inp:
** cube A (element 1) is held at its bottom nodes 1-4; cube B (element 2)
** stands beside it, its bottom nodes 11-14 tied in z by *EQUATION to nodes
** 1-4 and held in x and y; cube B's top carries 4 in -z
*NODE, NSET=NALL
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
5, 0., 0., 1.
6, 1., 0., 1.
7, 1., 1., 1.
8, 0., 1., 1.
11, 2., 0., 0.
12, 3., 0., 0.
13, 3., 1., 0.
14, 2., 1., 0.
15, 2., 0., 1.
16, 3., 0., 1.
17, 3., 1., 1.
18, 2., 1., 1.
*ELEMENT, TYPE=C3D8, ELSET=EALL
1, 1, 2, 3, 4, 5, 6, 7, 8
2, 11, 12, 13, 14, 15, 16, 17, 18
*NSET, NSET=FIX
1, 2, 3, 4
*NSET, NSET=BBOT
11, 12, 13, 14
*NSET, NSET=BTOP
15, 16, 17, 18
*MATERIAL, NAME=M
*ELASTIC
1000., 0.3
*SOLID SECTION, ELSET=EALL, MATERIAL=M
*EQUATION
2
11, 3, 1., 1, 3, -1.
2
12, 3, 1., 2, 3, -1.
2
13, 3, 1., 3, 3, -1.
2
14, 3, 1., 4, 3, -1.
*BOUNDARY
FIX, 1, 3
BBOT, 1, 2
*STEP
*STATIC
*CLOAD
BTOP, 3, -1.
*NODE PRINT, NSET=BBOT, TOTALS=YES
RF
*NODE PRINT, NSET=FIX, TOTALS=YES
RF
*END STEP
Variant rf_eq_free.inp: the equations end on four extra nodes 21-24 that belong to no element (same coordinates as nodes 1-4) and are held instead; nodes 1-4 stay held too (the diff lists the changed lines of rf_eq.inp; ... stands for unchanged lines):
18, 2., 1., 1.
+21, 0., 0., 0.
+22, 1., 0., 0.
+23, 1., 1., 0.
+24, 0., 1., 0.
...
-11, 3, 1., 1, 3, -1.
+11, 3, 1., 21, 3, -1.
(likewise 12 -> 22, 13 -> 23, 14 -> 24)
...
+*NSET, NSET=FREE
+21, 22, 23, 24
*BOUNDARY
FIX, 1, 3
+FREE, 1, 3
...
+*NODE PRINT, NSET=FREE, TOTALS=YES
+RF
rf_rb.inp (rigid body reference node) and its two variants
** C3D8 cube; its bottom nodes 1-4 form a rigid body with reference node 100,
** which is held; the top carries 4 in -z, so node 100 carries the whole load
*NODE, NSET=NALL
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
5, 0., 0., 1.
6, 1., 0., 1.
7, 1., 1., 1.
8, 0., 1., 1.
100, 0.5, 0.5, 0.
101, 0.5, 0.5, 0.
*ELEMENT, TYPE=C3D8, ELSET=EALL
1, 1, 2, 3, 4, 5, 6, 7, 8
*NSET, NSET=BOT
1, 2, 3, 4
*NSET, NSET=TOP
5, 6, 7, 8
*NSET, NSET=REF
100
*MATERIAL, NAME=M
*ELASTIC
1000., 0.3
*SOLID SECTION, ELSET=EALL, MATERIAL=M
*RIGID BODY, NSET=BOT, REF NODE=100, ROT NODE=101
*BOUNDARY
100, 1, 3
101, 1, 3
*STEP
*STATIC
*CLOAD
TOP, 3, -1.
*NODE PRINT, NSET=REF
RF
*END STEP
rf_rb_spring.inp: rf_rb.inp with a grounded spring on the held node 100 (it carries no force), these lines added to the model data (... stands for the lines of rf_rb.inp in between):
*ELEMENT, TYPE=SPRING1, ELSET=ESPR
2, 100
...
*SPRING, ELSET=ESPR
3
1000.
rf_rb_tr.inp: rf_rb.inp with an identity transform on node 100, these lines added before the *RIGID BODY card:
*TRANSFORM, NSET=REF
1., 0., 0., 0., 1., 0.
How to run
ccx rf_eq
ccx rf_eq_free
ccx rf_rb
ccx rf_rb_spring
ccx rf_rb_tr
Expected
In rf_eq the only supports in z are nodes 1-4 (FIX), so their RF must total +4 in z: 1.0 at each node, as the same supports give in rf_eq_free. (The dependent nodes 11-14, which are not held in z, print the MPC force, 1.0 each, in both decks. That is the element-force rule of 24334f7f, as at the pre-tension section nodes 83-86 and 94-97 of pret4.dat.ref, and not part of this report.)
In the rf_rb decks node 100 is the only support of the body, so RF at node 100 is (0, 0, 4).
Actual
rf_eq:
forces (fx,fy,fz) for set BBOT and time 0.1000000E+01
11 3.000000E-01 3.000000E-01 1.000000E+00
12 -3.000000E-01 3.000000E-01 1.000000E+00
13 -3.000000E-01 -3.000000E-01 1.000000E+00
14 3.000000E-01 -3.000000E-01 1.000000E+00
total force (fx,fy,fz) for set BBOT and time 0.1000000E+01
2.220446E-16 1.665335E-16 4.000000E+00
forces (fx,fy,fz) for set FIX and time 0.1000000E+01
1 0.000000E+00 0.000000E+00 0.000000E+00
2 0.000000E+00 0.000000E+00 0.000000E+00
3 0.000000E+00 0.000000E+00 0.000000E+00
4 0.000000E+00 0.000000E+00 0.000000E+00
total force (fx,fy,fz) for set FIX and time 0.1000000E+01
0.000000E+00 0.000000E+00 0.000000E+00
rf_eq_free: nodes 21-24 print 1.000000E+00 each in z, total 4.000000E+00, as expected.
RF at node 100:
rf_rb 100 -1.665335E-16 5.551115E-17 4.000000E+00
rf_rb_spring 100 0.000000E+00 -5.551115E-17 2.220446E-16
rf_rb_tr 100 0.000000E+00 0.000000E+00 0.000000E+00 L
Where it goes wrong
src/resultsforc.c builds the nodal forces fn (from which RF is printed) in two loops over the MPCs. The first (lines 40-58) moves the force of each MPC’s dependent DOF to its independent DOFs:
forcempc=fn[mt*node+ndir]/coefmpc[ist];
fmpc[i]=forcempc;
fn[mt*node+ndir]=0.;
...
fn[mt*node+ndir]-=coefmpc[index]*forcempc;
After the system force vector is formed, the second loop (“adding the mpc force again to fn”, lines 67-92) undoes this, but only for nodes that belong to an element:
if(iponoel[node]!=0){
fn[mt*node+ndir]=forcempc*coefmpc[ist];
}
...
if(iponoel[node]!=0){
fn[mt*node+ndir]+=coefmpc[index]*forcempc;
}
So at a node with an element, fn ends as the element forces alone and the MPC force is dropped; at a node without an element the MPC force stays. The two iponoel[node]!=0 tests came with 24334f7f (they replaced a special case for the extra nodes of MEANROT MPCs). The membership test is a stand-in for “the node is outside the structure”, and the cases here are where the two differ: in rf_eq the supports belong to cube A but take cube B’s load through equations, and in rf_rb_spring an inert spring puts node 100 in iponoel. It also explains the documented case of a reference node that belongs to the rigid body.
With a *TRANSFORM, src/bounadd.f turns the *BOUNDARY on node 100 into an MPC (“transformation applies: SPC is MPC in global carthesian coordinates”) whose dependent term is node 100’s own DOF. The first loop sets the force of that dependent DOF to 0, and the second loop does not put it back because node 100 has no element, so RF is 0 (as the output shows).
Fix
Possible fix (not built or tested), meant to keep the tests of 24334f7f: for DOFs that carry an SPC, print fn as it is after the first loop (element forces minus the forces the MPCs take away) whether or not the node belongs to an element, and give a dependent DOF that a transformed SPC created the force of the SPC (the force of its inhomogeneous term). Keep the current rule for all other DOFs. In beammrlinx_same/diff the held nodes (262-269) belong to no element, so they keep their RF; in pret4 the held nodes (29-32, 150-153) are not on the pre-tension section, and the section nodes carry no SPC, so pret4.dat.ref should not change either. If the element-only fn is needed elsewhere, keep a separate copy for the output.
Workaround: read reactions at nodes that belong to no element (an extra node held and tied by the equations, as in rf_eq_free; a reference node with no element and no *TRANSFORM), or check equilibrium from the applied loads.