Skip to content

fix(fem): a Sesam settlement reaches Sestra, one BLDEP term per dof pair, and an *MPC with instance-qualified nodes - #400

Closed
oleandor wants to merge 5 commits into
Krande:mainfrom
oleandor:fix/fem-reader-edge-cases
Closed

oleandor wants to merge 5 commits into
Krande:mainfrom
oleandor:fix/fem-reader-edge-cases

Conversation

@oleandor

Copy link
Copy Markdown
Contributor

Five items flagged during the constraint work (#393/#394) and left open because they were out of that scope. All five turned out to live on main, not in those PRs, so they are here, one commit each, independent of every open PR and merging cleanly with #393.

1. A settlement now reaches Sestra — BNDISPL, verified against Sestra V11.3-00

write_bcs.py said it in a comment: "PRESCRIBED = 2 — never written: ada's Bc magnitudes are not carried into BNDISPL yet." A Bc with a magnitude — a support settlement — silently became a fixed support on the Sesam route.

No Input Interface File manual ships with the install, so the record layout was not guessed: its field names came from Sestra's own diagnostics in Bin/DataAccess.dll ("Invalid DTYPE on BNDISPL card. Supported values are 1 (displacement) and 3 (acceleration)", "…too short for … dof count given in NDOF field") and the layout from solving for it. It is BNLOAD's shape: LLC DTYPE COMPLX 0 / NODENO NDOF D1 D2 / D3... Measured on a 1 m IPE300 cantilever with tip dz = -0.01:

deck tip Z from the .SIN
BNBCD fix code 2 + BNDISPL -0.009999999776 (float32's neighbour of -0.01 — a SIN stores nodal results single-precision), RY free, X = Y = 0
fix code 2, no BNDISPL Sestra: "No load is specified", no displacement result
BNDISPL, fix code 1 value ignored, Z = 0
fix code 2 + BNDISPL DTYPE 3 no displacement result

So a settlement is loading in Sesam — BNDISPL declares a load case — and the two cards are one statement. Only a non-zero magnitude counts (a prescribed zero is a clamp), so existing decks are byte-identical. The Sesam reader has no BNDISPL card, so the value does not survive the way back; the cross-format gap table now says that by name instead of the old "not carried yet".

2. BLDEP: one term per (slave_dof, master_dof), not the sum of both

_merged honours §7.2.14 ("the same combination of SLAVE and MASTER may occur only once") by concatenating a pair's terms into one record — so a (slave_dof, master_dof) appearing twice went out twice, and Sestra adds the betas: two equations declaring u(1,1) = 1.0·u(2,1) produced u(1,1) = 2.0·u(2,1). Decided from what produces it: the same beta twice (bit-for-bit, from the same arithmetic) is one dependency declared twice and is written once with a NOTE; different betas are two incompatible statements — a *Tie and an *Equation on one pair — and the later one is refused by name, OMITTED with both betas, the first written. Records now carry their constraint's name so the report can say which two.

3. *MPC with instance-qualified nodes no longer kills the import

BEAM, P-1.1, P-1.3 raised AttributeError: 'NodeProxy' object has no attribute 'members' — get_set_from_assembly resolves P-1.1 to the node, and mpc_nodes sent every non-int to .members. _read_fem guards that call against KeyError only, so the whole deck was lost. A set is expanded, anything else is taken as the node it is, so BEAM, P-1.1, P-1.3 and TIE, P-1.2, P-1.tip coexist.

4. A flat deck and an *Assembly deck read the same constraints — the premise was wrong, the test stays

The flag said a flat (non-*Assembly) deck's constraints were never read. Measured: they are, all five kinds, and have been since 2021 (101dfdf1d); seven flat-deck shapes probed. The two paths did disagree — the other way round, because item 3 lost the assembly deck. The parity test asked for is here anyway: the same five-constraint model flat and wrapped, equal names and types; it fails if either direction regresses.

5. An unsupported load is one OMITTED finding

load_str already reports OMITTED and returns "" (landed in #386) — nothing covered it, and a return None there brings the original distant TypeError: can only concatenate str (not "NoneType") straight back. Two tests pin it.

Verification

Every test was watched failing first, and each fix mutated: BLDEP back to concatenation (2 of 3 fail on the doubled term), nodes.extend(ref.members) restored (AttributeError at reader.py:1322), always-FIXED (3 fail incl. the Sestra one), bndispl_str returning "" (4 fail; Sestra writes no displacement field), load_str returning None (the TypeError), and the flat path's constraint read blanked (parity fails).

Suite tests/core: 4277 passed, 22 skipped, 0 failed (+12); tests/fem/test_sesam_prescribed_displacement.py runs Sestra for real. Lint clean. Re-run independently by the session opening this PR: 48 passed in the changed test files, the Sestra test passing with BNBCD code 2 and BNDISPL -1.0e-02 seen in the deck.

The user's Sestra-proven regression deck (rev32, 344 MB, COMPLETED_WITH_OMISSIONS (5 omitted, 1 approximated, 21188 note)) converted on this branch and on clean 0.91.0 differs by the timestamp only.


🤖 Generated with Claude Code

https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv

oleandor and others added 5 commits September 26, 2026 20:47
``*MPC`` inside an ``*Assembly`` names nodes as ``P-1.4``, which is what Abaqus/CAE writes for
every assembly-level MPC. ``get_set_from_assembly`` resolves that form to the *node* -- its
numeric branch returns ``part.fem.nodes.from_id(4)`` -- while ``mpc_nodes`` fed every
non-integer reference straight to ``ref.members``, so ``BEAM, P-1.1, P-1.3`` raised
``AttributeError: 'NodeProxy' object has no attribute 'members'``. ``_read_fem`` guards the
assembly-level constraint read against ``KeyError`` only, so the whole import died, not just the
constraint. The same model written flat, with bare node ids, read all four of its constraints.

``mpc_nodes`` now expands a ``FemSet`` and takes anything else as the node it is, so the set-name
form and the instance-qualified-id form can sit on data lines of one deck.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
…traints

Reported as "a flat (non-*Assembly) deck's constraints are never read". Measured: it reads them.
A deck with no *Assembly goes add_fem_without_assembly -> get_fem_from_bulk_str, which has called
get_constraints_from_inp since 2021, and the five-constraint deck below (tie, rigid body,
coupling, equation, MPC) comes back with all five on the part FEM. The two paths did disagree,
but the other way round: the *assembly* path lost the whole deck to an *MPC naming P-1.4, fixed
in the preceding commit.

So this is the test rather than a fix. Nothing held the two readers to the same answer, and which
layout a deck has is a CAE export option, not a modelling decision. Mutations run: reverting the
*MPC fix fails it (assembly side raises), and blanking the flat path's
``fem.constraints = get_constraints_from_inp(...)`` fails it too, so it covers both directions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
… sum of both

``_merged`` gives each (slave, master) pair one record, as the manual requires (BLDEP, section
7.2.14: "The same combination of SLAVE and MASTER may occur only once"), by concatenating the
terms of every record on that pair. The term list could then hold one (slave_dof, master_dof)
twice, and Sestra adds the betas of a repeated pair together: a deck declaring
``u(1,1) = 1.0 * u(2,1)`` twice was written as NDDOF 1 NDEP 2 with beta 1.0 on both triplets,
which is ``u(1,1) = 2.0 * u(2,1)``. Measured on the two-equation case in the new test: terms came
out ``((1, 1, 1.0), (1, 1, 1.0))``.

What produced the repeat decides what happens to it, and the two cases are not the same fault:

* the same beta twice (within 1e-12 relative; duplicates come from the same arithmetic and agree
  bit for bit) is one dependency declared twice -- an *Equation pasted in again, two Abaqus
  keywords the reader turned into one relation. Written once, and noted.
* two different betas are two incompatible statements about one dof, a *Tie and an *Equation over
  the same node pair being the usual source. Summing is what neither constraint says and nor is
  averaging, so the later one is refused by name (OMITTED, with both betas) and the
  first-declared relation is written.

The existing per-dof SUSPECT no longer claims "Sesam sums linear dependencies", which is now true
only of two records with different masters, and its key iteration is sorted so the report order is
deterministic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
…t TypeError

Reported as unfixed: ``write_loads`` raising a ``TypeError`` far from the load that caused it.
Measured: ``load_str`` already reports through ``conversion_report`` (fixed in Krande#386, no longer
``logger.error`` + ``return None``), but nothing covered it, so nothing would notice it coming
back. These pin it.

Both mutations were run against the tests. Restoring ``return None`` in place of the
OMITTED-and-``""`` reproduces the original failure exactly -- ``TypeError: can only concatenate
str (not "NoneType") to str`` at ``case_loads_str``'s ``out_str +=``, which names a string
concatenation and not the pressure load. Keeping ``return ""`` but dropping the ``rep.omitted``
call leaves a deck that writes and a report that says nothing: 0 findings where 1 is asserted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
``PRESCRIBED = 2`` was defined and never written -- "ada's Bc magnitudes are not carried into
BNDISPL yet" -- so a ``Bc`` with ``magnitudes`` came out of the Sesam writer as an ordinary clamp
and a settlement case became a rigid support: a different structure, and one whose reaction forces
are what a settlement case is usually run for.

No Input Interface File manual ships with the installed Sestra, so the record was not read off
one. Its fields come from Sestra V11.3-00's own diagnostics in ``Bin/DataAccess.dll`` -- "Invalid
DTYPE on BNDISPL card. Supported values are 1 (displacement) and 3 (acceleration)", "BNDISPL card
is too short for complex loads and dof count given in NDOF field", "Multiple BNDISPL cards are
given for a (load case number, internal node number) pair" -- and the layout from solving for it.
It is BNLOAD's shape: ``LLC DTYPE COMPLX 0`` / ``NODENO NDOF D1 D2`` / ``D3..Dndof``.

Measured, 1 m IPE300 cantilever clamped at x=0 with dz = -0.01 prescribed at the tip and no other
loading, Sestra V11.3-00 solving it (tests/fem/test_sesam_prescribed_displacement.py):

* FIX code 2 and the BNDISPL record: tip Z = -0.009999999776482582, which is float32's neighbour
  of -0.01 (a SIN stores nodal results single precision), tip RY = 6.907e-03 free, X = Y = 0.
* FIX code 2 and no BNDISPL: "WARNING ... No load is specified", and no displacement result at all.
* the BNDISPL record with FIX code 1 instead of 2: the value is ignored, tip Z = 0.0.

So the two cards are one statement and neither half is optional, and a settlement is *loading* in
Sesam -- BNDISPL declares an LLC. It is written in the load block, into the first load case, and a
model whose only loading is a settlement gets a load case opened for it. Only a nonzero magnitude
counts: a prescribed zero is a fixed support, so every existing deck is byte-identical.

``not_held.report_bcs`` drops from APPROXIMATED to NOTE for such a BC -- nothing is approximated
now -- but keeps saying it, because the value does not survive the way back: the Sesam reader has
no BNDISPL card, which is the cross-format gap this leaves behind and which its table now names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
@github-actions

Copy link
Copy Markdown

PR Review

👋 I checked your PR and found no issues. Thanks!

  • ✅ PR title is ok
  • ✅ Exactly one release label
  • ✅ SOURCE_KEY secret is set
  • ✅ Calculated next version: "0.91.1"

@github-actions

Copy link
Copy Markdown

🚀 Profiling Results (Top 20 most expensive calls)

Function Calls Duration (s)
<built-in method builtins.exec> (~:0) 1372 21.3433
<module> (<string>:1) 1 21.3433
test_build_big_ifc_pipe (tests/profiling/test_ifc_creation.py:48) 1 6.2595
test_bench_batch_tessellate (tests/profiling/test_cad_backend_bench.py:63) 1 6.2084
run (tests/profiling/test_cad_backend_bench.py:73) 6 6.0307
batch_tessellate (src/ada/visit/tessellate.py:1102) 1446 6.0295
__truediv__ (src/ada/api/spatial/part.py:2046) 8 6.0132
tessellate_geom (src/ada/visit/tessellate.py:830) 1440 5.4469
tessellate_occ_geom (src/ada/visit/tessellate.py:684) 1440 4.8525
tessellate_shape (src/ada/visit/tessellate.py:554) 1440 4.8389
to_ifc (src/ada/api/spatial/assembly.py:325) 5 4.6048
sync (src/ada/cadit/ifc/store.py:200) 5 4.3156
add_object (src/ada/api/spatial/part.py:309) 1201 4.2803
add_pipe (src/ada/api/spatial/part.py:164) 200 4.2616
sync_added_physical_objects (src/ada/cadit/ifc/write/write_ifc.py:92) 5 4.2607
add (src/ada/cadit/ifc/write/write_ifc.py:582) 2200 4.1807
tessellate (src/ada/cad/__init__.py:1187) 1440 3.8898
add_section (src/ada/api/spatial/part.py:294) 801 3.6556
add (src/ada/api/containers/sections.py:139) 805 3.6543
equal_props (src/ada/sections/concept.py:100) 241399 3.3530

@Krande

Krande commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Folded into #407.

This branch was merged in as-is, with its own merge commit, so every commit and its authorship
survive in that PR's history. Nothing here was rewritten or dropped.

#407 collects the six format-level FEA PRs (#394, #395, #396, #399, #400, #404) into one, since
adapy is squash-only and each merged feat:/fix: cuts a release. Closing this in favour of it.

@Krande Krande closed this Sep 27, 2026
Krande added a commit that referenced this pull request Sep 27, 2026
…nd verify plates through decks (#407)

Folds the six format-level FEA PRs into one, drops the Abaqus/CAE stack, and gives the verification
report a plate case built through the deck writers.

Folded: #394 (*Tie -> BLDEP facet interpolation), #395 (Sesam super element number), #396 (a
symmetric section's zero product of inertia), #399 (.gnx as a first-class format), #400 (Sesam
settlement, one BLDEP term per dof pair, instance-qualified *MPC), #404 (surface pressure as BEUSLO).

Dropped: #398, #401, #402, #403, #405 -- src/ada/cadit/cae, Part.to_abaqus_cae_script, its tests and
verification/genie_vs_abaqus, which drives `abaqus cae noGUI=`. adapy supports formats, not APIs into
proprietary software, and a verification entry only a CAE licence can reproduce verifies nothing
anyone can check. Salvaged from it, because it is format-level: a T-profile now reaches an Abaqus
*Beam Section as section=I with the bottom flange zeroed (it had no mapping at all), and Calculix
converts a T to a general section as it already did an I.

New: the plate strip case -- an ada.Plate meshed by the meshing module (gmsh), written as a deck per
solver, checked against 5 q L^4 / (384 D) and f_n = n^2 pi / (2 L^2) sqrt(D / (rho t)). Code_Aster
lands on the eigen closed form to six digits.

Writer gaps it exposed, each found by running it rather than reading:

* Calculix had no pressure load at all; it now writes *DLOAD ... P. Measured on ccx 2.23 that P, P1
  and P2 are bit-identical on a shell, so the label carries no face and only the sign can.
* Code_Aster's pressure had never run: FORCE_FACE with FY is a traction along global Y, not a normal
  pressure, and GROUP_MA named the surface, which has no MED counterpart -- every attempt stopped at
  <EXCEPTION> <MODELISA7_77>. Now FORCE_COQUE=_F(PRES=...) over the element sets, via the new shared
  ada.fem.surfaces.pressure_elsets.
* Code_Aster refused any eigenvalue analysis with more than one Bc; ASSEMBLAGE's CHARGE takes a tuple.
* The Sesam BEUSLO pressure sign was inverted relative to Abaqus, Calculix and Code_Aster. The
  reference it was tuned against applied its load along global -z on the grounds that -z was
  "against the plate's +z normal"; the strip's element normals are all -z (measured, all 128), so the
  reference ran along the normal and the test that pinned it never checked which face was named.
  Code_Aster reproduces the Sestra magnitude to eight digits with the opposite sign. The measured
  Sestra numbers are kept exactly as measured; only which face they belong to changed. The Sestra leg
  runs only where Sestra is installed, so a licensed re-run remains the confirmation worth having.

Co-Authored-By: oleandor <oleandor@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXGBkUbGK6oYWxm5R2WYVB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants