*FREQUENCY / *BUCKLE after a nonlinear step: an *EQUATION that a *RIGID BODY or knot MPC depends on is not decascaded again, wrong eigenvalues (exit 0, no message)

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 *TRANSFORM on one clamped node gives the right frequencies, because any *TRANSFORM makes 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: *STEP instead of *STEP, NLGEOM in 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

  1. In a nonlinear step, cascade.c treats RIGID, KNOT, PLANE, BEAM and STRAIGHT MPCs 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=2 and 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;
  1. nonlingeo.c keeps 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 returns icascade=2 in mpcinfo[2].

  2. In the next step, CalculiX.c:707 sets icascade=0 for a linear step, but CalculiX.c:1176-1178 decascades 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.

Confirmed (using official build of ccx 2.23 on Windows), needs a GitHub ticket.

eq_rigid.dat:

     E I G E N V A L U E   O U T P U T

 MODE NO    EIGENVALUE                       FREQUENCY   
                                     REAL PART            IMAGINARY PART
                           (RAD/TIME)      (CYCLES/TIME     (RAD/TIME)

      1   0.7052614E+08   0.8397984E+04   0.1336581E+04   0.0000000E+00
      2   0.6722124E+09   0.2592706E+05   0.4126420E+04   0.0000000E+00
      3   0.1845021E+10   0.4295371E+05   0.6836295E+04   0.0000000E+00