Skip to content

umat: add a CalculiX external-behaviour interface for the native umat_user hook - #51

Open
petlenz wants to merge 3 commits into
mainfrom
feature/calculix-interface
Open

petlenz wants to merge 3 commits into
mainfrom
feature/calculix-interface

Conversation

@petlenz

@petlenz petlenz commented Sep 14, 2026

Copy link
Copy Markdown
Member

Adds the .so entry point CalculiX dlopens for *MATERIAL, NAME=@LIB,FUNC, translating CalculiX's native call_external_umat_user conventions onto the existing umat_dispatch.

Why a second entry point

CalculiX has two external hooks and they do not share a convention:

  • call_external_umat is reached from umat_abaqus.f after that wrapper has already converted to the Abaqus UMAT convention. NUMSIM_MATERIALS_DEFINE_UMAT already serves it — no adapter needed.
  • call_external_umat_user is the native hook. It speaks CalculiX's own conventions, which this adapter translates.

What the adapter translates

Concern CalculiX native Conversion
Constant count kode = -100 - nconst nprops = -kode - 100
emec/emec0 tensorial, {11,22,33,12,13,23}, end/start ×2 shear → engineering; STRAN=emec0, DSTRAN=emec-emec0
stre PK2, in/out passed 1:1
stiff(21) upper-tri, symmetric symmetrized 6×6 → stiff[i+j*(j+1)/2]
xstateini/xstate full (nstate_, mi(1), #elem) arrays sliced at nstatv*((iint-1) + mi1*(iel-1))
TIME step time at increment end rebased to increment start

Each conversion mirrors CalculiX's own reference wrapper umat_abaqus.f (shear doubling at :280-283, TIME at :187-188, stiff packing at :335-355).

Guards rather than silent omissions

iorien != 0 and a nonzero beta are refused, not ignored. The native hook makes frame rotation the material's own responsibility (umat_user.f:86-104) — unlike the Abaqus path, where ccx rotates around the call — so ignoring either would return wrong results with no symptom.

Scope is documented as geometrically linear: the PK2/Green-Lagrange pairing is exact only for a linear C:E law.

Review

REVIEW.md records the three-lens review this came out of. It found three bugs that would each have produced a silently wrong result:

  • state addressed at point (1,1) for every integration point;
  • the tangent transposed for major-asymmetric materials;
  • TIME short by (time - dtime) on every increment after the first.

Every Critical/High finding was re-verified against the CalculiX 2.22 source — one reviewer's "the packing is correct" was itself wrong. Each of the three has a regression test that was verified to fail against the pre-fix code before the fix was restored, so none is vacuous.

Testing

11 new tests; 291/291 pass on this branch, clean build, no warnings. Coverage includes a 40-increment J2 run at iel=2, iint=3 (asserting no other state block is touched), asymmetric-tangent packing, exact TIME values via a probe material, nonzero start strain, and four error paths.

Not covered

  • No end-to-end run against real ccx through the @LIB,FUNC deck path yet.
  • No adapter-level PNEWDT cutback test (the path is inherited unchanged from umat_dispatch).
  • ielas == 1 is accepted and ignored; harmless for elastic, an inelastic model will need to honour it.

…_user hook

CalculiX has two external hooks and they do not share a convention.
call_external_umat is reached from umat_abaqus.f after that wrapper has already
converted to the Abaqus UMAT convention, so NUMSIM_MATERIALS_DEFINE_UMAT already
serves it. call_external_umat_user is the native hook, and this adapter
translates its conventions onto umat_dispatch:

  - kode carries the constant count as -100 - nconst;
  - emec/emec0 are TENSORIAL, so the shear is doubled to the engineering form
    the dispatch buffer expects, exactly as umat_abaqus.f:280-283 does;
  - xstateini/xstate are the FULL (nstate_, mi(1), #elements) arrays, not a
    slice, so state is indexed at nstatv*((iint-1) + mi1*(iel-1));
  - TIME is rebased onto the start of the increment per umat_abaqus.f:187-188;
  - the 6x6 tangent is symmetrized into stiff(21) as umat_abaqus.f:335-355 does,
    reading the column-major buffer narrow_matrix writes.

A local orientation and a nonzero initial stress are refused rather than
silently ignored: the native hook makes frame rotation the material's own
responsibility, so returning unrotated results would be wrong with no symptom.
Scope is geometrically linear; the PK2/Green-Lagrange pairing is exact only for
a linear C:E law.

REVIEW.md records the three-lens review this came out of, including the three
silent-wrong-answer bugs it found. Each has a regression test verified to fail
against the pre-fix code.
Four gaps found by re-reviewing this branch against the real ccx 2.22 source.
The conversions themselves held up -- all eight rows of the table, the 31-arg
ABI, the stiff(21) packing and the TIME rebasing were re-derived from the
Fortran and are correct -- so these are the edges.

ielas is now refused rather than ignored. ielas = 1 asks for a response with no
irreversible deformation; arpack.c, arpackbu.c and arpackcs.c all set it, which
is every *BUCKLE and *FREQUENCY step. Nothing here can suppress plastic flow on
request, so answering anyway folds irreversible effects into the tangent and
skews the extracted eigenvalues with no symptom. It joins iorien and beta in
the refuse-rather-than-guess group.

The emec/emec0 null guard had no test. REVIEW.md's resolution table listed M6
as fixed, and deleting the guard left all eleven tests passing -- a claim, not a
behaviour. With the new test the binary segfaults instead, which is the guard's
whole point.

A fatal tripped outside a FatalProbe aborted the entire test binary. The
default handler calls std::abort(), which is right in a solver -- returning
would hand the host a silently wrong material response -- and wrong in a test
run, where it masks every other result. Mutating the kode decode by one was
enough to lose the whole suite to a SIGABRT. A binary-wide handler now fails
the current test instead; verified by mutation to give exit 1 with the other 15
tests still running. FatalProbe restores that handler rather than the library
default, so the first probe in a run no longer re-arms abort() for everything
after it.

The header said nothing about how the .so is found. ccx prepends "lib",
appends ".so" and dlsyms FUNC verbatim, which is why FUNC carries no trailing
underscore here while the Abaqus entry point is umat_ -- get it backwards and
it fails at run time with "unable to load function" and nothing at build time.
Also recorded: the symbol must stay exported (-fvisibility=hidden hides it with
no other symptom), and ccx builds the library name into a fixed char b[80] with
an unchecked memcpy, so the LIB name must be short.

Two tests written during the review are ported in, closing the last two items
REVIEW.md listed as still open: a throwing material proves PNEWDT = 0.25 lands
on the CALLER's buffer for a cutback, and eight threads driving the entry point
at distinct (iel, iint) must agree with the same sequences run serially.

Every one of the four is mutation-verified: removing the ielas guard, the null
guard, the pnewdt forwarding, or the isolation handler each fails exactly its
own test. 16/16 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant