Source code for femorph_solver.mapdl_api.cdb

"""Load MAPDL CDB input decks into an femorph-solver :class:`~femorph_solver.Model`.

Thin wrapper around :mod:`mapdl_archive`.  Only the file-format reader
is involved — MAPDL itself is never invoked.

The CDB's ``ET`` table (MAPDL element-type declarations such as
``ET, 1, 186``) is translated on load so the returned Model already
knows each ET id's kernel — callers don't have to re-declare them.

References
----------
* CDB format definition (MAPDL input deck, produced by the
  ``CDWRITE`` command): Ansys, Inc., *Ansys Mechanical APDL
  Command Reference*, Release 2022R2, entry ``CDWRITE``; and
  *Ansys Mechanical APDL Basic Analysis Guide*, chapter on the
  database file.
* Open-source parser: ``mapdl-archive``
  (https://github.com/akaszynski/mapdl-archive, MIT) — the
  production parser we delegate to.  Does not invoke MAPDL, only
  reads the text grammar.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING, Any

from femorph_solver._labels import UnitSystem, unit_system_from_mapdl

if TYPE_CHECKING:
    from femorph_solver.model import Model


#: MAPDL element-type-number → registered kernel name.  Covers every
#: type femorph-solver ships a kernel for; a deck that uses an
#: unrecognised type silently skips the declaration (the caller gets
#: a clean "element type N not declared" error the first time they
#: try to use it).  Keys match the numeric codes stored in
#: ``mapdl_archive.Archive.ekey``.
_MAPDL_NUM_TO_NAME: dict[int, str] = {
    185: "SOLID185",
    186: "SOLID186",
    187: "SOLID187",
    188: "BEAM188",
    181: "SHELL181",
    182: "PLANE182",
    180: "LINK180",
    14: "COMBIN14",
    21: "MASS21",
}


# ``/UNITS,<token>`` or ``/UNITS,<token>,...`` — whitespace allowed
# around the comma; token is the leading comma-separated field.  CDB
# parsers (including MAPDL itself) only look at the first token.
_UNITS_RE = re.compile(r"^\s*/UNITS\s*,\s*([A-Za-z0-9_]+)", re.IGNORECASE)


def _detect_unit_system(path: str | Path) -> UnitSystem:
    """Scan the CDB for a ``/UNITS,<token>`` command and return its :class:`UnitSystem`.

    Returns :attr:`UnitSystem.UNSPECIFIED` when no ``/UNITS`` command
    appears in the first 8 KiB of the file (the header region).  MAPDL
    decks place ``/UNITS`` near the top; scanning further down would
    just slow this call without improving accuracy.
    """
    try:
        with open(path, encoding="utf-8", errors="ignore") as fh:
            head = fh.read(8192)
    except OSError:  # pragma: no cover - defensive; from_cdb opens again
        return UnitSystem.UNSPECIFIED
    for line in head.splitlines():
        m = _UNITS_RE.match(line)
        if m:
            return unit_system_from_mapdl(m.group(1))
    return UnitSystem.UNSPECIFIED


[docs] def from_cdb(path: str, **kwargs: Any) -> Model: """Load a MAPDL CDB file via :mod:`mapdl_archive`. Requires the ``mapdl`` extra: ``pip install femorph_solver[mapdl]``. The deck's ``/UNITS`` command (if present) is parsed and stamped onto :attr:`Model.unit_system`. Decks without a ``/UNITS`` line get :attr:`UnitSystem.UNSPECIFIED`. """ try: import mapdl_archive except ImportError as exc: # pragma: no cover - import-guard path raise ImportError( "Reading MAPDL CDB decks requires the 'mapdl' extra. " "Install with: pip install femorph_solver[mapdl]" ) from exc from femorph_solver.model import Model archive = mapdl_archive.Archive(path, **kwargs) model = Model.from_grid(archive.grid) model.set_unit_system(_detect_unit_system(path)) # Register the CDB's ET table on the Model so callers don't have # to re-declare element types by hand. ``archive.ekey`` is a # ``(n_etypes, 2)`` array whose rows are ``[et_id, mapdl_type_num]``; # unknown numbers are skipped silently (later use raises a clear # "element type N not declared" error). ekey = getattr(archive, "ekey", None) if ekey is not None: for row in ekey: et_id = int(row[0]) mapdl_num = int(row[1]) name = _MAPDL_NUM_TO_NAME.get(mapdl_num) if name is not None: # Use the private impl so this internal MAPDL-side # registration doesn't emit the Model.et() deprecation # warning at every ``from_cdb`` call. model._et_impl(et_id, name) return model