Repository navigation
fix(gxml, sat): an HP section keeps its flange on read, and a plate outline keeps its start vertex through the SAT - #406
Conversation
PR Review👋 I checked your PR and found no issues. Thanks!
|
🚀 Profiling Results (Top 20 most expensive calls)
📈 Performance Impact per Commit
|
|
Reviewed. Both fixes are right, and the outline one is a genuinely nice piece of work — I checked The HP mirror is equally clearly correct: it is what Two things before this goes in. 1. Rebase — the base is about to be squashed awayThis is stacked on #399, and #407 folds #399 (with #394, #395, #396, #400, #404) into a single squash 2. The
|
``from_genie_xml`` read every bulb flat back with half its flange dimensions unset. ``HP200x10`` is written as ``<l_section h="0.2" b="0.038" tw="0.01" tf="0.0224">`` and came back as ``w_top=None, t_ftop=None`` against a source that has ``w_top=0.038, t_ftop=0.0224``. The writer is not at fault and must not change: ``l_section`` with exactly ``h``/``b``/``tw``/``tf`` is what GeniE itself writes for an HP. The V8.4-06 export in ``files/fem_files/sesam/xml_all_basic_props.xml`` carries ``<section name="HP140x8"><l_section h="0.14" b="0.019" tw="0.008" tf="0.0123" .../>`` -- four attributes, which is all the element has and all an ANGULAR shape needs. Adding a fifth would produce a file GeniE ignores. The reader is: ``read_sections.angular`` was the one section reader that did not mirror its single written width/thickness onto both flange slots. ``isec``, ``box_sec``, ``channel_section`` and ``bar_section`` all do, and ``profile_db_collect`` -- which builds every HP in the profile DB -- sets ``w_top == w_btn`` and ``t_ftop == t_fbtn``. So ``angular`` now mirrors too. Why it hid for so long: nothing in the ANGULAR geometry path reads the top flange. ``calc_angular`` and ``sections.profiles.angular`` use only ``h``, ``w_btn``, ``t_w`` and ``t_fbtn``, so every ``GeneralProperties`` value round-tripped exactly while ``unique_props()`` differed -- and that is the only section comparison that survives a file boundary, ``Section.__eq__`` being guid identity. It was not merely cosmetic. ``BoxSides._get_dim`` (bounding_box.py:220) takes ``max(section.w_btn, section.w_top)`` for a beam, so measuring any HP beam read from a Genie XML raised ``TypeError: '>' not supported between instances of 'NoneType' and 'float'``. It now returns ``(2.0, 0.038, 0.2)``. Swept rather than spot-fixed: ``test_gxml_section_round_trip.py`` round-trips every example in ``BaseTypes.get_valid_example_map_lists()`` and compares the eight dimensions, all 20 ``GeneralProperties`` fields and ``unique_props()``. Measured before the fix, HP was the only shape that lost anything; BOX, TUB, I, T, UNP and FB were already exact. ``BaseTypes.CIRCULAR`` has no writer branch at all -- the ``<section>`` is dropped while the beam's ``section_ref`` is still written, so the read fails with ``ValueError: The section id "CIRC100" is not found``. That pre-existing gap is now pinned by name instead of being invisible; it is not fixed here. One asymmetry is pinned rather than closed: ``string_to_section``'s ``L`` branch leaves ``w_top``/``t_ftop`` unset where ``profile_db_collect`` fills them, so ``L150x10`` now reads back with the equal legs it already has (0.15/0.01) instead of ``None``. Those are the correct values and the ones ``_get_dim`` needs; the remaining inconsistency is in the string parser, not in this reader, and closing it would change ``unique_props()`` for L sections everywhere. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
A plate written to Genie XML read back rotated by one vertex. Measured on the
unit square: ``poly.points3d`` of ``(0,0,0) (0,1,0) (1,1,0) (1,0,0)`` came
back as ``(1,0,0) (0,0,0) (0,1,0) (1,1,0)`` -- same loop, same winding, same
normal, entered one corner later. Identical on the ``.gnx`` and plain ``.xml``
paths, and on non-convex outlines (an L and a C rotated by exactly one step
too), so not a container or a tessellation effect.
The reader is not at fault. ``PlateFactory.get_edges_from_loop`` walks the
coedge ring from ``loop.coedge`` and ``get_points`` returns it in that order;
dumping the SAT showed the loop the reader reads is exactly the loop the
writer wrote. The SAT also has room to carry the authored start, because
``plate_to_sat_entities`` already makes ``outline[0]`` the loop's first coedge
(``coedge_ids = [loop.coedge if i == 0 else ...]``). So the start vertex is
preserved here rather than canonicalised in the reader.
The writer is: ``outline_ccw_about`` rewinds the outline so the loop agrees
with the face normal (ACIS takes the material side from that) and did it with
``pts[::-1]``. Reversing a closed loop that way is the same cyclic loop
entered one step earlier -- ``[v0, v1, v2, v3]`` becomes ``[v3, v2, v1, v0]``
-- and that one step is the whole defect. ``CurvePoly2d._points_fix`` rewinds
the same way and keeps ``points[0]`` (``[points[0]] + reversed(points[1:])``),
which is why the read plate's own rewind could not undo it: it rewound about
the already-rotated start. Reversing about the first vertex instead --
``[v0, v3, v2, v1]`` -- makes the two agree and the round trip exact.
Geometrically a no-op: a rotation of a closed loop is the same loop with the
same winding and the same face, so ``winding_dots`` stays ``[1.0]`` and the
Genie reference comparisons in ``test_write_write_basic_sat.py``, which are
cyclic by design ("record numbering is positional and carries no meaning"),
are unaffected.
Covered at both levels: the writer's contract (the rewind may only rotate, and
only to the rotation that keeps ``[0]``; the emitted body's loop starts where
the outline does) and the end-to-end Genie XML round trip, over a square each
way round, a rectangle and two non-convex outlines. Winding and normal are
asserted separately from order, since a reversal would satisfy a cyclic
comparison while flipping which side of the plate is material; the C-shape
also checks enclosed area, which a scrambled order would lose.
Not covered: a plate with a hole. ``plate_to_sat_entities`` authors a single
loop and never sets ``Loop.next_loop``, so the flat-plate path cannot express
one -- the reader handles multi-loop faces (``_get_primary_loop`` picks
periphery over hole) because Genie-authored files have them, but nothing on
this writer path produces one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
…from its string and from every deck The HP fix in the previous commit made the Genie XML reader mirror an ``l_section``'s single ``b``/``tf`` onto ``w_top``/``w_btn`` and ``t_ftop``/``t_fbtn``, as ``profile_db_collect`` builds every HP. That exposed the same asymmetry one level up: ``string_to_section``'s ``L`` branch set only ``w_btn``/``t_fbtn``. Before the HP fix an ``L150x10`` went ``(w_top, t_ftop) = (None, None) -> (None, None)`` through Genie XML and ``unique_props()`` matched by accident; after it, ``(None, None) -> (0.15, 0.01)`` and it did not -- invisible to the round-trip sweep, whose ANGULAR examples held only ``HP180x10``. The L branch now passes ``w_top=w, w_btn=w, t_ftop=t_w, t_fbtn=t_w``. The parser only reads equal-leg angles (``L<h>x<t>``; the leg is set equal to ``h``), so the mirrored value is the leg the section already has. Inert for geometry, measured for ``L150x10`` before and after: all 20 ``GeneralProperties`` fields identical, the profile outline identical, and the written section lines identical -- Abaqus ``section=L`` ``0.15, 0.15, 0.01, 0.01``, Sesam ``GBEAMG``/``GLSEC``, Genie ``<l_section h="0.15" b="0.15" tw="0.01" tf="0.01">`` and the Code_Aster ``.comm``. ``calc_angular`` and ``profiles.angular`` read only ``h``, ``w_btn``, ``t_w``, ``t_fbtn``; the Sesam ``GLSEC`` writer already fell back from ``w_top`` to ``w_btn``. What does move: the fields themselves and ``unique_props()``; the IFC ``ADA_SectionParameters`` bag gains ``w_top``/``t_ftop``; the Code_Aster ``.adapy_fem.json`` sidecar writes 0.15/0.01 where it wrote null; and ``BoxSides._get_dim`` for an L beam built from its string returns ``(2.0, 0.15, 0.15)`` where it raised ``TypeError: '>' not supported between instances of 'NoneType' and 'float'``. The Abaqus (``section=L``) and Sesam (``GLSEC``) readers mirror too. Both filled only the bottom slots -- the Sesam one on purpose, to match the L string parser -- so the full suite's one failure with the parser fix alone was ``test_every_zoo_model_round_trips_whole[elements_line_profiles]`` (``angle/profile/w_top`` and ``t_ftop`` "only in original"). Measured before the reader change, an ``HP180x10`` also read back from either deck as ``w_top=None, t_ftop=None``: a pre-existing mismatch for every HP, the same one the Genie XML reader had, now closed with the L one. Unequal-leg angles are not parsed: the pattern is ``L<number>x<number>``, unanchored (``re.search``), so ``L150x100x10`` silently reads as ``L150x100`` -- an equal-leg angle 150 mm with a 100 mm thickness. Nothing here mirrors a leg that is not equal; that misparse is pre-existing and left alone. ``L150x10`` joins the ANGULAR list in ``get_valid_example_map_lists()`` so the Genie XML sweep covers it, and ``test_an_equal_leg_angle_gains_the_legs_its_string_parser_omits`` is gone, as its own first assertion asked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv
ed3e3c2 to
ff712dc
Compare
|
Thanks. Both points are done and force-pushed. 1. Rebased onto post-#407
|
| before | after | |
|---|---|---|
(w_top, t_ftop) |
(None, None) |
(0.15, 0.01) |
all 20 GeneralProperties fields |
Ax 0.0029, Iy = Iz 6.37244e-06, Iyz -3.80172e-06, ... | identical |
profile outline (get_section_profile) |
6 points | identical |
Abaqus *Beam Section, section=L |
0.15, 0.15, 0.01, 0.01 |
identical |
Sesam GBEAMG + GLSEC |
GLSEC 1 0.15 0.01 0.15 / 0.01 1 1 1 |
identical |
GeniE <l_section> |
h="0.15" b="0.15" tw="0.01" tf="0.01" |
identical |
Code_Aster .comm (VALE = (A, IY, IZ, JX)) |
identical | |
BoxSides._get_dim() for Beam(..., "L150x10") |
TypeError: '>' not supported ... 'NoneType' and 'float' |
(2.0, 0.15, 0.15) |
What did move, and why each change is correct:
unique_props()of every string-built L now carries the two top-flange values. That is the point of the fix.- The IFC
ADA_SectionParametersbag now includesw_top/t_ftop, because it writes every non-Nonedimension. It round-trips. - The Code_Aster
.adapy_fem.jsonsidecar writes0.15/0.01where it used to writenull. - The Sesam
GLSECwriter already fell back fromw_toptow_btn, so its card doesn't change. Searching forw_top is None/t_ftop is Nonefound only those two Sesam fallbacks (GLSEC, GCHAN). The onlyw_top or 0.0istopology/run_builder.py, which is for duct/tray sections, not angles.
The Abaqus and Sesam readers mirror too. This was the one suite failure with the parser fix alone: test_every_zoo_model_round_trips_whole[elements_line_profiles] (angle/profile/w_top, t_ftop: "only in original"). The Abaqus section=L reader and the Sesam GLSEC reader filled only the bottom slots. The Sesam one did so on purpose, per its docstring, to match the old L parser. Measured before the reader change, an HP180x10 also read back from either deck as w_top=None, t_ftop=None. That is the same pre-existing HP mismatch the GeniE reader had, so the readers now mirror the one flange onto both slots too. The ANGULAR geometry still reads only the bottom slots, so no property moves. New tests/core/fem/formats/test_angular_section_round_trip.py covers {abaqus, sesam} × {HP180x10, L150x10} on unique_props().
Unequal legs. The parser doesn't support them. The pattern is L<number>x<number> and unanchored (re.search), and the leg is always set equal to h. So L150x100x10 silently parses as L150x100: an equal-leg 150 angle with a 100 mm thickness (h=0.15, w_btn=0.15, t_w=t_fbtn=0.1, Ax = 0.02). The mirror only copies a leg the parser already treats as equal, so it creates no new unequal-leg case. The misparse is pre-existing and I've left it alone. It may deserve its own issue.
Sweep and tests. L150x10 is now in BaseTypes.ANGULAR's list in get_valid_example_map_lists(), so the GeniE sweep covers it: dimensions, all properties and unique_props(). test_an_equal_leg_angle_gains_the_legs_its_string_parser_omits is deleted. test_an_equal_leg_angle_carries_its_leg_in_both_flange_slots pins the fields and the _get_dim result for an L built from its string.
Mutations: 9, all caught (targeted set: 133 tests, green baseline asserted before and after)
| mutation | failures |
|---|---|
| L mirror removed (both slots) | 6: sweep L150x10 dims + unique_props, _get_dim test, abaqus/sesam L round trip, Abaqus zoo |
L: only w_top filled |
6 (same set) |
L: only t_ftop filled |
6 (same set) |
Abaqus reader mirror removed / only w_top |
3 each: abaqus HP + L, zoo |
Sesam reader mirror removed / only w_top |
2 each: sesam HP + L |
| GeniE reader HP mirror removed (re-run after rebase) | 6, now including the sweep's L150x10 |
SAT rewind back to pts[::-1] (re-run after rebase) |
17 |
tests/core/: 4534 passed, 22 skipped, 16 xfailed, 0 failed, 0 errors. black (120), isort and ruff are clean.
Split out as issues
- GeniE XML writer drops a CIRCULAR section but still writes the beam's section_ref #409: the GeniE writer drops a
CIRCULARsection but still writes the beam'ssection_ref. - SAT plate writer cannot author holes (Loop.next_loop never set), while the reader handles multi-loop faces #410: the SAT plate writer cannot author holes (
Loop.next_loopnever set) while the reader handles multi-loop faces. I measured this rather than taking it from the note: a plate with aPrimBoxboolean writes one loop on both the imprint and unfused paths, and it reads back with 0 booleans and nothing logged.
🤖 Generated with Claude Code
Two GeniE round-trip defects, found by #399's round-trip test and pinned there by name because they were the reader's, not the container's. Both root causes measured; both pinned notes removed, and that test now catches both.
Rebased onto post-#407 main (0.92.0), three commits: the HP fix, the SAT outline fix, and — per review — the L-section asymmetry closed at its source in
string_to_section, with the Abaqussection=Land SesamGLSECreaders mirroring the same way (anHP180x10read back from either deck had the sameNoneflange the GeniE reader had). ForL150x10everyGeneralPropertiesfield, the profile outline and every written deck line are identical before and after; only the two fields,unique_props()andBoxSides._get_dim(aTypeErrorbefore) move. Details in the comment below.1. An
HP200x10lostw_top/t_ftopon read — the readerGeniE itself writes a bulb flat as an
l_sectionwith exactly four attributes — its own V8.4-06 export infiles/fem_files/sesam/xml_all_basic_props.xmlhas<l_section h="0.14" b="0.019" tw="0.008" tf="0.0123"/>— so the writer is right and must not change.read_sections.angularwas the only section reader that did not mirror its single width/thickness onto both flange slots the wayisec,box_sec,channel_sectionandbar_sectiondo (and the wayprofile_db_collectbuilds every HP). Two lines.It hid because nothing in the ANGULAR geometry path reads the top flange, so every
GeneralPropertiesvalue already round-tripped. It was not cosmetic:BoxSides._get_dimdoesmax(w_btn, w_top), so measuring any HP beam read from a GeniE XML raisedTypeError. Now(2.0, 0.038, 0.2).2. A plate outline came back rotated by one vertex — the writer
The SAT was dumped and the reader walked:
PlateFactory.get_edges_from_loopreturns the loop in the order the writer wrote it — the reader is blameless.outline_ccw_aboutinsat/write/write_plate.pyrewinds an outline to agree with the face normal usingpts[::-1], and reversing a closed loop that way is the same cycle entered one step earlier ([v0,v1,v2,v3]→[v3,v2,v1,v0]); sinceoutline[0]becomes the loop's first coedge, that step is the defect.CurvePoly2d._points_fixrewinds identically but keepspoints[0], so the read plate could not undo it. Fix, one line at the single choke point all three write paths share:np.roll(pts[::-1], 1, axis=0)— reverse about the first vertex. The start vertex is now preserved through the SAT; no reader canonicalisation, no test expectation rotated.Coverage
test_gxml_section_round_trip.py(44): every one of the 13 supportedBaseTypesexamples × dimensions, all 20GeneralPropertiesfields,unique_props(), the bbox consequence, and a self-maintaining check that the unsupported set equals whatget_section_propsrefuses. Measured pre-fix: HP was the only shape losing anything.test_gxml_plate_outline_round_trip.py(11) andtest_write_plate_outline_start.py(10): square in both windings, rectangle, L, C — exact vertex order, winding and normal asserted separately; a guard that the SAT path was actually used.files/fem_files/sesam/), read and read→write→read, before vs after: counts identical, every section'sIy/Iz/Axidentical; the only diffs are the two intended ones. Outline fidelity on a round trip: 15/15 broken before, 0/15 after.Mutations — 5, all caught
Both mirrors dropped (5 tests); only
t_ftopdropped — the half fix (4);pts[::-1]restored (17, incl. the.gnxround trip); roll by 2 — same cycle, wrong start (17: proves the start is asserted, not the cycle); winding condition inverted (14, incl. four pre-existing GeniE-reference tests).Suite
tests/core/: 4381 passed, 22 skipped, 0 failed; lint clean. Re-run independently by the session opening this PR: 290/290 across gxml, sat and the new files.Found and pinned, not fixed
BaseTypes.CIRCULARhas no branch in the GeniE writer'sget_section_props: the<section>is dropped while the beam'ssection_refis still written, so the file is internally inconsistent and only the reader complains. Pre-existing; now pinned by name.plate_to_sat_entitiesauthors a single loop and never setsLoop.next_loop; the reader handles multi-loop faces, the writer cannot produce one.string_to_section'sLbranch leaves the top-flange fields unset whereprofile_db_collectfills them; anL150x10now reads back with its equal legs rather thanNone. Correct values; closing the asymmetry would changeunique_props()for every L section.🤖 Generated with Claude Code
https://claude.ai/code/session_01Nej3bWxJevs9GkH6tg1WKv