Skip to content

feat(med): integrate MED 4.1 bitmask field tracker into writer#8

Open
fznoussi wants to merge 1 commit into
mainfrom
feat/med-v4-bitmask-field-tracker
Open

feat(med): integrate MED 4.1 bitmask field tracker into writer#8
fznoussi wants to merge 1 commit into
mainfrom
feat/med-v4-bitmask-field-tracker

Conversation

@fznoussi
Copy link
Copy Markdown
Collaborator

This commit integrates the FieldBitmaskWriter introduced in _med41.py into the main writer in _med.py and adds the med_to_geo_type translation table required to map internal MED short codes (PO1, SE2, TR3, ...) to their fully qualified MED 4.1 geometric type names (MED_POINT1, MED_SEG2, MED_TRIA3, ...).

Context: why the tracker needs to be called from _med.py

The bitmask attributes defined by MED 4.1 (LEN, LGC, LCA, ...) must be written on the field group after all time steps and all entity/geo type combinations have been written. This requires a two-phase approach:

phase 1 - notify(): called once per (entity_type, geo_type, step)
as each data group is created inside _write_data()
phase 2 - flush(): called once per field after all writes are done
to commit the accumulated bitmasks to the HDF5 field group

The tracker is therefore instantiated once per write() call and shared across all _write_data() calls for that file.

Fix A : Add med_to_geo_type translation table

The FieldBitmaskWriter works with fully qualified MED 4.1 type names (MED_POINT1, MED_SEG2, ...) while the rest of the writer uses short MED codes (PO1, SE2, ...). A new translation table bridges the two:

med_to_geo_type = {
PO1: MED_POINT1,
SE2: MED_SEG2, SE3: MED_SEG3, SE4: MED_SEG4,
TR3: MED_TRIA3, TR6: MED_TRIA6, TR7: MED_TRIA7,
QU4: MED_QUAD4, QU8: MED_QUAD8, QU9: MED_QUAD9,
TE4: MED_TETRA4,T10: MED_TETRA10,
HE8: MED_HEXA8, H20: MED_HEXA20,H27: MED_HEXA27,
PY5: MED_PYRA5, P13: MED_PYRA13,
PE6: MED_PENTA6,P15: MED_PENTA15,PE18:MED_PENTA18,
POG: MED_POLYGON,POG2:MED_POLYGON2,
}

Fix B : Instantiate FieldBitmaskWriter in write()

A single FieldBitmaskWriter instance is created at the start of the write pass and passed as the tracker argument to every _write_data() call:

tracker = FieldBitmaskWriter()

nodal data

for name, data in mesh.point_data.items():
tracker.notify(MED_NODE, MED_POINT1, step)
_write_data(..., tracker=tracker)

cell data

for name, d in mesh.cell_data.items():
for cell, data in zip(mesh.cells, d):
med_type = meshio_to_med_type[cell.type]
_write_data(..., med_type, tracker=tracker)
for field_name in fields.keys():
tracker.flush(fields[field_name]) # commit bitmasks
Fix C : Pass tracker through _write_data() signature

The tracker parameter (defaulting to None) was added to _write_data() to allow the caller to inject the shared FieldBitmaskWriter instance. When tracker is None the function behaves exactly as before, preserving full backward compatibility with all existing call sites and tests.

Fix D - flush() placement: once per field, after all steps written

flush() is called inside the cell_data loop after all cell blocks for a given field name have been written:

for name, d in mesh.cell_data.items():
for cell, data in zip(mesh.cells, d):
_write_data(...)
for field_name in fields.keys(): # flush after all blocks
tracker.flush(fields[field_name])

This ensures that the bitmasks reflect the complete set of entity and geometry types for the field before being committed to the HDF5 file. Writing the bitmasks before all steps are done would produce incomplete masks that would cause MED 4.1 readers to skip valid data.

Tests added

Unit tests for primitive bit operations (_med41.py)

test_bit_set
Verifies that _bit_set correctly sets individual bits at positions
0, 1 and 3 and that the resulting uint32 value matches the expected
binary representation.

test_bit_test
Verifies that _bit_test correctly detects set and unset bits on a
mask with bits 0 and 2 active (0b00101).

Unit tests for decode functions (_med41.py)

test_decode_entity_mask_empty
A zero mask returns an empty list.

test_decode_entity_mask_node
Bit 3 set (0b001000 = 8) decodes to [MED_NODE].

test_decode_entity_mask_cell
Bit 0 set (0b000001 = 1) decodes to [MED_CELL].

test_decode_entity_mask_multiple
Bits 0 and 3 set (0b001001 = 9) decodes to both MED_CELL and
MED_NODE. Order and completeness are both asserted.

test_decode_geo_mask_triangle
MED_TRIA3 is at index 4 in _GEO_ORDER[MED_CELL], so bit 4
(0b010000 = 16) must decode to [MED_TRIA3].

test_decode_geo_mask_empty
A zero geo mask returns an empty list.

Unit tests for FieldBitmaskWriter

test_bitmask_writer_notify_node
After notify(MED_NODE, MED_NO_GEOTYPE, step), bit 3 of
_g_entity must be set (MED_NODE = bit 3).

test_bitmask_writer_notify_cell
After notify(MED_CELL, MED_TRIA3, step), bit 0 of _g_entity
must be set (MED_CELL = bit 0) and bit 4 of _g_geo[MED_CELL]
must be set (MED_TRIA3 = index 4 in _GEO_ORDER[MED_CELL]).

test_bitmask_writer_notify_multiple_steps
Two notify() calls on different steps must update _s_entity
independently. step1 must have only bit 3 set (MED_NODE) and
step2 must have only bit 0 set (MED_CELL).

Integration tests (HDF5 level)

test_bitmask_writer_flush (tmp_path)
Creates an HDF5 file, calls notify(MED_NODE, ...) then flush()
and verifies directly in the HDF5 file that:
- LEN attribute exists and has bit 3 set (MED_NODE)
- LNA attribute exists (step count for MED_NODE geo mask)
- LAA attribute exists and equals 1 (one step where all types present)

test_bitmask_written_in_real_med_file (tmp_path)
Writes a complete mesh with point_data using meshio.med.write()
and verifies in the resulting HDF5 file that for every field in CHA:
- LEN attribute is present
- LAA attribute is present
- bit 3 of LEN is set (MED_NODE present, since data is nodal) This is the highest-level test and covers the full write pipeline including tracker instantiation, notify() and flush() calls.

What is unchanged

  • The HDF5 structure written for nodes, cells, families and profiles is identical to the previous version. The bitmask attributes are purely additive and do not affect readers that do not support MED 4.1.
  • _write_data() internal logic is unchanged. The tracker parameter is optional and the function can still be called without it.
  • The read() path is unchanged. read_field_types() from _med41.py is available for future use but is not yet called during read().

This commit integrates the FieldBitmaskWriter introduced in _med41.py
into the main writer in _med.py and adds the med_to_geo_type translation
table required to map internal MED short codes (PO1, SE2, TR3, ...) to
their fully qualified MED 4.1 geometric type names (MED_POINT1,
MED_SEG2, MED_TRIA3, ...).

─────────────────────────────────────────────────────────────────────
Context: why the tracker needs to be called from _med.py
─────────────────────────────────────────────────────────────────────
The bitmask attributes defined by MED 4.1 (LEN, LGC, LCA, ...) must
be written on the field group after all time steps and all entity/geo
type combinations have been written. This requires a two-phase approach:

  phase 1 - notify(): called once per (entity_type, geo_type, step)
            as each data group is created inside _write_data()
  phase 2 - flush():  called once per field after all writes are done
            to commit the accumulated bitmasks to the HDF5 field group

The tracker is therefore instantiated once per write() call and shared
across all _write_data() calls for that file.

─────────────────────────────────────────────────────────────────────
Fix A - Add med_to_geo_type translation table
─────────────────────────────────────────────────────────────────────
The FieldBitmaskWriter works with fully qualified MED 4.1 type names
(MED_POINT1, MED_SEG2, ...) while the rest of the writer uses short
MED codes (PO1, SE2, ...). A new translation table bridges the two:

  med_to_geo_type = {
      PO1: MED_POINT1,
      SE2: MED_SEG2,  SE3: MED_SEG3,  SE4: MED_SEG4,
      TR3: MED_TRIA3, TR6: MED_TRIA6, TR7: MED_TRIA7,
      QU4: MED_QUAD4, QU8: MED_QUAD8, QU9: MED_QUAD9,
      TE4: MED_TETRA4,T10: MED_TETRA10,
      HE8: MED_HEXA8, H20: MED_HEXA20,H27: MED_HEXA27,
      PY5: MED_PYRA5, P13: MED_PYRA13,
      PE6: MED_PENTA6,P15: MED_PENTA15,PE18:MED_PENTA18,
      POG: MED_POLYGON,POG2:MED_POLYGON2,
  }

─────────────────────────────────────────────────────────────────────
Fix B - Instantiate FieldBitmaskWriter in write()
─────────────────────────────────────────────────────────────────────
A single FieldBitmaskWriter instance is created at the start of the
write pass and passed as the tracker argument to every _write_data()
call:

  tracker = FieldBitmaskWriter()

  # nodal data
  for name, data in mesh.point_data.items():
      tracker.notify(MED_NODE, MED_POINT1, step)
      _write_data(..., tracker=tracker)

  # cell data
  for name, d in mesh.cell_data.items():
      for cell, data in zip(mesh.cells, d):
          med_type = meshio_to_med_type[cell.type]
          _write_data(..., med_type, tracker=tracker)
      for field_name in fields.keys():
          tracker.flush(fields[field_name])   # commit bitmasks

─────────────────────────────────────────────────────────────────────
Fix C - Pass tracker through _write_data() signature
─────────────────────────────────────────────────────────────────────
The tracker parameter (defaulting to None) was added to _write_data()
to allow the caller to inject the shared FieldBitmaskWriter instance.
When tracker is None the function behaves exactly as before, preserving
full backward compatibility with all existing call sites and tests.

─────────────────────────────────────────────────────────────────────
Fix D - flush() placement: once per field, after all steps written
─────────────────────────────────────────────────────────────────────
flush() is called inside the cell_data loop after all cell blocks for
a given field name have been written:

  for name, d in mesh.cell_data.items():
      for cell, data in zip(mesh.cells, d):
          _write_data(...)
      for field_name in fields.keys():       # flush after all blocks
          tracker.flush(fields[field_name])

This ensures that the bitmasks reflect the complete set of entity and
geometry types for the field before being committed to the HDF5 file.
Writing the bitmasks before all steps are done would produce incomplete
masks that would cause MED 4.1 readers to skip valid data.

─────────────────────────────────────────────────────────────────────
Tests added
─────────────────────────────────────────────────────────────────────
Unit tests for primitive bit operations (_med41.py)

  test_bit_set
    Verifies that _bit_set correctly sets individual bits at positions
    0, 1 and 3 and that the resulting uint32 value matches the expected
    binary representation.

  test_bit_test
    Verifies that _bit_test correctly detects set and unset bits on a
    mask with bits 0 and 2 active (0b00101).

Unit tests for decode functions (_med41.py)

  test_decode_entity_mask_empty
    A zero mask returns an empty list.

  test_decode_entity_mask_node
    Bit 3 set (0b001000 = 8) decodes to [MED_NODE].

  test_decode_entity_mask_cell
    Bit 0 set (0b000001 = 1) decodes to [MED_CELL].

  test_decode_entity_mask_multiple
    Bits 0 and 3 set (0b001001 = 9) decodes to both MED_CELL and
    MED_NODE. Order and completeness are both asserted.

  test_decode_geo_mask_triangle
    MED_TRIA3 is at index 4 in _GEO_ORDER[MED_CELL], so bit 4
    (0b010000 = 16) must decode to [MED_TRIA3].

  test_decode_geo_mask_empty
    A zero geo mask returns an empty list.

Unit tests for FieldBitmaskWriter

  test_bitmask_writer_notify_node
    After notify(MED_NODE, MED_NO_GEOTYPE, step), bit 3 of
    _g_entity must be set (MED_NODE = bit 3).

  test_bitmask_writer_notify_cell
    After notify(MED_CELL, MED_TRIA3, step), bit 0 of _g_entity
    must be set (MED_CELL = bit 0) and bit 4 of _g_geo[MED_CELL]
    must be set (MED_TRIA3 = index 4 in _GEO_ORDER[MED_CELL]).

  test_bitmask_writer_notify_multiple_steps
    Two notify() calls on different steps must update _s_entity
    independently. step1 must have only bit 3 set (MED_NODE) and
    step2 must have only bit 0 set (MED_CELL).

Integration tests (HDF5 level)

  test_bitmask_writer_flush (tmp_path)
    Creates an HDF5 file, calls notify(MED_NODE, ...) then flush()
    and verifies directly in the HDF5 file that:
    - LEN attribute exists and has bit 3 set (MED_NODE)
    - LNA attribute exists (step count for MED_NODE geo mask)
    - LAA attribute exists and equals 1 (one step where all types present)

  test_bitmask_written_in_real_med_file (tmp_path)
    Writes a complete mesh with point_data using meshio.med.write()
    and verifies in the resulting HDF5 file that for every field in CHA:
    - LEN attribute is present
    - LAA attribute is present
    - bit 3 of LEN is set (MED_NODE present, since data is nodal)
    This is the highest-level test and covers the full write pipeline
    including tracker instantiation, notify() and flush() calls.

─────────────────────────────────────────────────────────────────────
What is unchanged
─────────────────────────────────────────────────────────────────────
- The HDF5 structure written for nodes, cells, families and profiles
  is identical to the previous version. The bitmask attributes are
  purely additive and do not affect readers that do not support MED 4.1.
- _write_data() internal logic is unchanged. The tracker parameter is
  optional and the function can still be called without it.
- The read() path is unchanged. read_field_types() from _med41.py is
  available for future use but is not yet called during read().
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