Summary
Manual 7.118 (*SHELL SECTION) says a composite layer without an orientation takes the one given by the ORIENTATION parameter, if any. Manual 6.2.14 adds that a layer with no orientation gets the default shell axes, from the global x-axis projected on the shell. In shellsections.f, however, iorientation is set once to the section’s ORIENTATION. Every layer line that names an orientation overwrites it, and nothing resets it. So a later layer line without an orientation silently gets the previous layer’s orientation.
In the deck below, plies OR90 / (none) / OR90 run as 90/90/90 instead of 90/0/90. The same happens when the section itself has an ORIENTATION. Master (9ba5d76) still has the bug: shellsections.f was last changed on 6 Oct 2026, for ANISO_CREEP/JOHNSONCOOK names.
Version
ccx 2.23 (conda-forge build pl5321h484372f_2, macOS arm64).
Already reported or fixed?
Not reported: there is no matching GitHub issue (open or closed) or open pull request in Dhondtguido/CalculiX, and no calculix.discourse.group topic (searched 19 Oct 2026). Not fixed: the routine involved is unchanged on master, and conda-forge’s latest 2.23 build (pl5321hd17ca8b_8, which carries about 40 extra patches) gives the same output.
Minimal input
plies_blank.inp is one S8R (unit square) of a unidirectional material: E1 = 100000, E2 = E3 = 10000, three plies of 1/3, stretched by 0.1 % along x.
*HEADING
One S8R, three plies: OR90 / no orientation / OR90; uniaxial stretch along x
*NODE
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
5, 0.5, 0., 0.
6, 1., 0.5, 0.
7, 0.5, 1., 0.
8, 0., 0.5, 0.
*ELEMENT, TYPE=S8R, ELSET=EALL
1, 1, 2, 3, 4, 5, 6, 7, 8
*NSET, NSET=NALL
1, 2, 3, 4, 5, 6, 7, 8
*NSET, NSET=X0
1, 4, 8
*NSET, NSET=X1
2, 3, 6
*MATERIAL, NAME=UD
*ELASTIC, TYPE=ENGINEERING CONSTANTS
100000., 10000., 10000., 0.3, 0.3, 0.3, 5000., 5000.,
5000.
*ORIENTATION, NAME=OR90
0., 1., 0., -1., 0., 0.
*SHELL SECTION, ELSET=EALL, COMPOSITE
0.333333333333, , UD, OR90
0.333333333333, , UD
0.333333333333, , UD, OR90
*BOUNDARY
X0, 1, 1
1, 2, 2
NALL, 3, 3
*STEP
*STATIC
*BOUNDARY
X1, 1, 1, 0.001
*NODE PRINT, NSET=X1, TOTALS=ONLY
RF
*END STEP
The reference plies_explicit.inp is the same deck with the middle ply naming the global axes:
*ORIENTATION, NAME=OR0
1., 0., 0., 0., 1., 0.
...
0.333333333333, , UD, OR0
How to run
ccx plies_blank and ccx plies_explicit
Expected
Layup [90/0/90]. Laminate theory gives Nx = (A11 - A12^2/A22) * 0.001 = 40.23 per unit width. ccx gives this for plies_explicit.inp: total RF_x on X1 = 4.022304E+01.
Actual
plies_blank.inp: total RF_x on X1 = 1.000000E+01. That is the [90/90/90] answer: laminate theory gives 10.00, and the same deck with all three plies naming OR90 also gives 1.000000E+01.
With *SHELL SECTION, ELSET=EALL, COMPOSITE, ORIENTATION=OR0 and the middle ply line left without an orientation, ccx also gives 1.000000E+01, where 4.022304E+01 is expected.
Where it goes wrong
src/shellsections.f (2.23 and master). iorientation is set from the ORIENTATION parameter before the layer loop (lines 121-135). Inside the loop (lines 313-331) it changes only when a layer names an orientation:
! reading the orientation, if any
! if no orientation is specified, the global orientation defined
! by the ORIENTATION parameter, if any, will be used
!
read(textpart(4)(1:80),'(a80)',iostat=istat) orientation
!
if(orientation(1:1).ne.' ') then
do i=1,norien
if(orname(i).eq.orientation) exit
enddo
...
iorientation=i
endif
Then ielorien(nlayer,...)=iorientation (lines 357 and 384) stores whatever is left from the previous layer. The comment in the code describes the documented behaviour.
Fix
Keep the section’s orientation and start every layer from it, e.g. with a new integer iorientation0:
! before the layer loop (nlayer=0)
iorientation0=iorientation
...
! for each layer, before reading its orientation
iorientation=iorientation0
read(textpart(4)(1:80),'(a80)',iostat=istat) orientation
Workaround: give every layer line an orientation, using an identity *ORIENTATION for the global axes, as in plies_explicit.inp.