Summary
A model has an *EQUATION whose dependent DOF is also a term of a nonlinear MPC: here the reference node of a *RIGID BODY, or a shell node that ccx made a knot. In a nonlinear first step (NLGEOM, no load), ccx prints *INFO in cascade: linear MPCs and nonlinear MPCs depend on each other and keeps the MPCs undecascaded, as intended for a nonlinear step. In the next step, a *FREQUENCY or *BUCKLE step, ccx does not decascade them again, and the eigenvalues come out wrong without any message (exit 0).
Equivalent inputs give different answers, which shows that this is a bug:
- the same deck with a linear first step (still no load) gives the right frequencies;
- the same deck with an identity
*TRANSFORMon one clamped node gives the right frequencies, because any*TRANSFORMmakes ccx decascade in every step.
Version
ccx 2.23 (macOS arm64), OMP_NUM_THREADS=1. Source: master 9ba5d76 (16 Oct 2026).
Already reported or fixed?
Not reported: there is no matching GitHub issue (open or closed) or pull request in Dhondtguido/CalculiX, and no calculix.discourse.group topic (all 1,234 topic titles and keyword searches checked, 21 Oct 2026). Not fixed: the decascading condition in CalculiX.c (line 1178) and cascade.c are unchanged on master (9ba5d76).
Related but different: CalculiX crashes on an *EQUATION between the nodes of two rigid bodies · Issue #178 · Dhondtguido/CalculiX · GitHub (closed: an *EQUATION between two rigid bodies crashed ccx in a single nonlinear step; here every step runs and the linear step after it is wrong).
Minimal input
eq_rigid.inp:
** C3D8 cantilever 200 x 20 x 20 clamped at x = 0; its tip face is a *RIGID BODY;
** an *EQUATION ties the reference node's z to node 6 (x = 100, z = 20)
*NODE
1, 0., 0., 0.
2, 0., 0., 20.
3, 0., 20., 0.
4, 0., 20., 20.
5, 100., 0., 0.
6, 100., 0., 20.
7, 100., 20., 0.
8, 100., 20., 20.
9, 200., 0., 0.
10, 200., 0., 20.
11, 200., 20., 0.
12, 200., 20., 20.
100, 200., 10., 10.
101, 200., 10., 10.
*ELEMENT, TYPE=C3D8, ELSET=E
1, 1, 5, 7, 3, 2, 6, 8, 4
2, 5, 9, 11, 7, 6, 10, 12, 8
*NSET, NSET=FIX
1, 2, 3, 4
*NSET, NSET=TIP
9, 10, 11, 12
*MATERIAL, NAME=M
*ELASTIC
210000., 0.3
*DENSITY
7.8e-9
*SOLID SECTION, ELSET=E, MATERIAL=M
*RIGID BODY, NSET=TIP, REF NODE=100, ROT NODE=101
*EQUATION
2
100, 3, 1., 6, 3, -1.
*BOUNDARY
FIX, 1, 3
*STEP, NLGEOM
*STATIC
*END STEP
*STEP
*FREQUENCY
3
*END STEP
Twins:
eq_rigid_lin.inp:*STEPinstead of*STEP, NLGEOMin step 1.eq_rigid_tr.inp: these lines inserted before*EQUATION(an identity transformation on a clamped node):
*NSET, NSET=T
1
*TRANSFORM, NSET=T
1., 0., 0., 0., 1., 0.
How to run
ccx eq_rigid, ccx eq_rigid_lin, ccx eq_rigid_tr; compare the eigenvalue output in the .dat files.
Expected
The frequencies of the twins, which agree with each other:
| mode | eq_rigid_lin (Hz) |
eq_rigid_tr (Hz) |
|---|---|---|
| 1 | 1336.581 | 1336.581 |
| 2 | 2601.245 | 2601.245 |
| 3 | 5176.374 | 5176.374 |
Actual
eq_rigid (exit 0, no warning):
| mode | eq_rigid (Hz) |
vs. the twins |
|---|---|---|
| 1 | 1336.581 | same |
| 2 | 4126.420 | +58.6 % |
| 3 | 6836.295 | +32.1 % |
In step 2 the output goes straight to “Determining the structure of the matrix:”; in eq_rigid_tr that line follows a new “Decascading the MPC’s”.
*BUCKLE is affected the same way: with step 2 replaced by *BUCKLE (2 factors) and *CLOAD 100, 1, -1000., the first factor is 434.1789, 3.6 % lower than the 450.3799 of the *TRANSFORM twin (the twin with a linear first step gives 450.3799 too). The second factor agrees (1729.632 vs 1729.636).
The same with knots: an S4 cantilever whose tip nodes ccx makes knots (2669.851 Hz instead of 461.6533 Hz)
eq_knot.inp (the B31 elements only make ccx create knots at shell nodes 3 and 6):
** S4 cantilever 20 x 10 x 0.2 clamped at x = 0; the B31 beams 3 and 4 only make tip nodes 3 and 6 knots
** one *EQUATION ties the tips' z; step 1: NLGEOM static without load; step 2: *FREQUENCY
*NODE
1, 0, 0, 0
2, 10, 0, 0
3, 20, 0, 0
4, 0, 10, 0
5, 10, 10, 0
6, 20, 10, 0
903, 20.1554563176, 0.155456317551, 0.155456317551
904, 20.1554563176, 10.1554563176, 0.155456317551
*ELEMENT, TYPE=S4, ELSET=E
1, 1, 2, 5, 4
2, 2, 3, 6, 5
*NSET, NSET=FIX
1, 4
*MATERIAL, NAME=M
*ELASTIC
210000., 0.3
*DENSITY
7.8e-9
*SHELL SECTION, ELSET=E, MATERIAL=M
0.2
*ELEMENT, TYPE=B31, ELSET=KN
3, 3, 903
4, 6, 904
*MATERIAL, NAME=KN
*ELASTIC
210000., 0.
*DENSITY
0.
*BEAM SECTION, ELSET=KN, MATERIAL=KN, SECTION=RECT
0.1346, 0.1346
1., -1., 0.
*EQUATION
2
6, 3, 1., 3, 3, -1.
*STEP, NLGEOM
*STATIC
1., 1.
*BOUNDARY
FIX, 1, 6
*END STEP
*STEP
*FREQUENCY
3
*END STEP
| mode 1 (Hz) | mode 2 (Hz) | mode 3 (Hz) | |
|---|---|---|---|
eq_knot |
2669.851 | 9891.557 | 19014.72 |
| step 1 without NLGEOM | 461.6533 | 5008.907 | 19014.72 |
identity *TRANSFORM on node 1 |
461.6533 | 5008.907 | 19014.72 |
Where it goes wrong
- In a nonlinear step,
cascade.ctreatsRIGID,KNOT,PLANE,BEAMandSTRAIGHTMPCs as nonlinear (lines 104-117: all MPCs count as linear only if*iperturb<2). When a nonlinear MPC has an independent term that is the dependent DOF of another MPC and the call comes from main, it sets*icascade=2and skips the substitution (cascade.c:162-178):
/* no expansion if there is a dependence of a
nonlinear MPC on another linear or nonlinear MPC
and the call is from main */
if((jmpc[mpc-1]==1)||(nl==1)){
*icascade=2;
-
nonlingeo.ckeeps a copy of these undecascaded MPCs (lines 787-795), cascades them per iteration, and restores the undecascaded copy at the end of the step (lines 4269-4277). It returnsicascade=2inmpcinfo[2]. -
In the next step,
CalculiX.c:707setsicascade=0for a linear step, butCalculiX.c:1176-1178decascades only when
/* decascading MPC's only necessary if MPC's changed */
if(((istep==1)||(ntrans>0)||(mpcend<0)||(nk!=nkold)||(nmpc!=nmpcold))&&(icascade==0)){
None of these hold here, so the linear step uses MPCs in which the *EQUATION’s dependent DOF (node 100, DOF 3) is still an independent term of the rigid-body MPCs. With any *TRANSFORM (ntrans>0) the condition holds and the result is right. How the matrix assembly then treats that term was not traced.
Fix
Decascade again when the previous step left the MPCs undecascaded, e.g. remember the value before line 707:
icascadeprev=icascade;
if((abs(nmethod)!=1)||(iperturb[0]<2))icascade=0;
...
if(((istep==1)||(ntrans>0)||(mpcend<0)||(nk!=nkold)||(nmpc!=nmpcold)||
(icascadeprev==2))&&(icascade==0)){
Workaround: an identity *TRANSFORM on any node (for example a clamped one). Where the eigenvalue step does not need the nonlinear step’s base state, running it first also avoids the problem.