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: "HEX8",
186: "HEX20",
187: "TET10",
188: "BEAM2",
181: "QUAD4_SHELL",
182: "QUAD4_PLANE",
180: "TRUSS2",
14: "SPRING",
21: "POINT_MASS",
}
# ``/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