Source code for endf_parserpy.tape.material

############################################################
#
# Author(s):       Georg Schnabel
# Email:           g.schnabel@iaea.org
# Creation date:   2026/05/15
# Last modified:   2026/05/17
# License:         MIT
# Copyright (c) 2026 International Atomic Energy Agency (IAEA)
#
############################################################

"""Per-material state and views for :class:`EndfFile`."""


def _as_mfmt(key):
    """Validate and normalise an ``(MF, MT)`` section key."""
    if not (isinstance(key, tuple) and len(key) == 2):
        raise KeyError(
            "a section is addressed by an (MF, MT) pair, e.g. material[3, 2]"
        )
    mf, mt = key
    return int(mf), int(mt)


class _MaterialSlot:
    """Mutable per-material state in an :class:`EndfFile`.

    A slot represents one material in the (possibly edited) tape. For a
    material that came from the file ``original_position`` indexes the
    backing :class:`TapeIndex`; for a material added in memory it is
    ``None``. ``overlay`` holds sections that were set or added (keyed
    by ``(MF, MT)``), and ``deleted`` the ``(MF, MT)`` keys removed from
    the original material. ``removed`` is set when the material itself
    is deleted from the tape, which invalidates any :class:`MaterialView`
    bound to the slot.
    """

    # a class attribute, so it defaults to False without per-instance
    # storage and survives unpickling of slots written before it existed
    removed = False

    def __init__(self, original_position=None, mat=None, za=None, awr=None):
        self.original_position = original_position
        self.mat = mat
        self.za = za
        self.awr = awr
        self.overlay = {}
        self.deleted = set()

    @property
    def is_modified(self):
        """Whether the slot differs from its original on-disk material."""
        return bool(self.overlay or self.deleted or self.original_position is None)


[docs] class MaterialView: """A handle to one material of an :class:`EndfFile`. Sections are addressed by an ``(MF, MT)`` pair and parsed lazily on access. A section can be read, replaced, added or deleted:: section = material[3, 2] material[3, 2] = edited_section del material[3, 2] Reading a section returns a *view* over the parsed section whose mutability follows the file's ``check_edits`` mode -- read-only under ``"eager"`` and a live write-through view under ``"deferred"`` (see :mod:`endf_parserpy.tape.views`). A section that failed to parse raises :class:`~endf_parserpy.tape.SectionParseError` on access. The view is bound to the underlying material, so it stays valid if the tape is reordered; it becomes invalid only if that material is deleted, after which every operation on it raises :class:`RuntimeError`. """ def __init__(self, endf_file, slot): self._file = endf_file self._slot = slot def _check_live(self): """Raise if the material this view is bound to has been deleted.""" if self._slot.removed: raise RuntimeError("this material has been deleted from the tape") @property def position(self): """The material's current zero-based position on the tape.""" self._check_live() return self._file._position_of(self._slot) @property def mat(self): """ENDF MAT number of the material.""" self._check_live() return self._slot.mat @property def za(self): """ZA identifier of the material, or ``None`` if unknown.""" self._check_live() return self._slot.za @property def awr(self): """Atomic weight ratio of the material, or ``None`` if unknown.""" self._check_live() return self._slot.awr @property def is_modified(self): """Whether this material has been edited.""" self._check_live() return self._slot.is_modified
[docs] def sections(self): """Return the list of ``(MF, MT)`` section keys of this material.""" self._check_live() return self._file._slot_section_keys(self._slot)
[docs] def to_tape_dict(self): """Return this material as a complete single-material tape dict. The result is a ``{MF: {MT: section}}`` mapping that, unlike the per-section access of this view, also carries an ``MF=0``/ ``MT=0`` tape-head (TPID) entry. It therefore forms a complete single-material tape and can be handed directly to the ordinary parser's ``write`` -- ``parser.write(view.to_tape_dict())`` yields a tape the ordinary ``parser.parse`` round-trips. Untouched sections appear as their raw ENDF-6 lines and edited or added ones as parsed mappings; the writer accepts both. """ self._check_live() return self._file._assemble(self._slot)
def __getitem__(self, key): self._check_live() mf, mt = _as_mfmt(key) section = self._file._get_slot_section(self._slot, mf, mt) return self._file._view(self._slot, mf, mt, section) def __setitem__(self, key, value): self._check_live() self._file._set_slot_section(self._slot, *_as_mfmt(key), value) def __delitem__(self, key): self._check_live() self._file._delete_slot_section(self._slot, *_as_mfmt(key)) def __contains__(self, key): self._check_live() try: mfmt = _as_mfmt(key) except KeyError: return False return mfmt in self._file._slot_section_keys(self._slot) def __iter__(self): self._check_live() return iter(self._file._slot_section_keys(self._slot)) def __len__(self): self._check_live() return len(self._file._slot_section_keys(self._slot)) def __repr__(self): if self._slot.removed: return f"<MaterialView (deleted) MAT={self._slot.mat}>" return ( f"<MaterialView position={self.position} " f"MAT={self._slot.mat} ZA={self._slot.za}>" )