2. Generate a Material Card from a Paramete

Why a Parameter Dictionary Is the Right Intermediate Representation

In LS-DYNA keyword files, a material definition is a fixed-format record: a keyword line, an optional title line, and one or more parameter lines whose columns carry positional meaning. Hand-editing these cards is error-prone because a single misplaced comma or a missing blank field shifts every subsequent value. The practical automation pattern is to separate data from serialization: keep material parameters in a plain Python dictionary keyed by LS-DYNA field names, then run a single generator function that knows the exact field order and formatting rules for the target keyword. This case builds that generator for *MAT_ELASTIC.

The dictionary approach pays off immediately. Downstream tools (spreadsheet exports, JSON configs, DOE sweeps, unit-conversion utilities) can all produce the same dictionary shape, and only one function needs to know how *MAT_ELASTIC is laid out. When you later add *MAT_PLASTIC_KINEMATIC or *MAT_JOHNSON_COOK, you add a new field-order table, not a new parser.

The *MAT_ELASTIC Card Layout

For LS-DYNA's *MAT_ELASTIC (material type 1), the card consists of:

  • Keyword line: *MAT_ELASTIC (or *MAT_ELASTIC_TITLE when a title line is present).
  • Title line (optional): a free-text description, conventionally starting in column 1.
  • Card 1, fields 1–8: MID, RO, E, PR, DA, DB, K, VC.

Fields are comma-separated. Trailing empty fields may be omitted, but interior empty fields must be represented by consecutive commas so that positional meaning is preserved. LS-DYNA accepts both comma-delimited and fixed-column (10-character) input; comma-delimited output is far easier to generate correctly and is what we will emit.

Field semantics for *MAT_ELASTIC: MID = material ID, RO = mass density, E = Young's modulus, PR = Poisson's ratio, DA/DB = axial damping constants (usually 0), K = bulk modulus (leave blank to let LS-DYNA derive it from E and PR), VC = viscous damping coefficient (usually 0).

Field Order and Formatting Rules the Generator Must Enforce

The generator's job is to translate a dictionary into a string that LS-DYNA will parse without warnings. Three rules dominate:

  1. Order is positional, not alphabetical. The dictionary may hold keys in any order; the generator must emit them in the canonical *MAT_ELASTIC order.
  2. Missing optional fields become empty strings, not zeros. Writing 0 for K would tell LS-DYNA the bulk modulus is literally zero, which is physically wrong. An empty field lets the solver compute it.
  3. Numbers must be formatted without scientific notation surprises. LS-DYNA accepts 1.0e-9 but not 1E-09 in every build; use lowercase e and a leading digit before the decimal point.
Parameter dictionary to *MAT_ELASTIC card: field mapping and formatting pipeline Parameter Dictionary (order-independent) "MID": 101 "RO": 7.85e-9 "E": 210000.0 "PR": 0.3 "DA": 0.0 "DB": 0.0 "VC": 0.0 "K": (omitted) "TITLE": "Steel_AISI_4340" Canonical Field Order *MAT_ELASTIC card 1 1. MID ← material ID 2. RO ← density 3. E ← Young's modulus 4. PR ← Poisson's ratio 5. DA ← axial damping 6. DB ← axial damping 7. K ← bulk modulus 8. VC ← viscous damping Missing optional → empty field Trailing empties may be trimmed Interior empties → consecutive commas Emitted Card comma-delimited, lowercase e *MAT_ELASTIC_TITLE Steel_AISI_4340 $# MID RO E 101,7.85e-9,210000.0,0.3, 0.0,0.0,,0.0 Field 7 (K) is empty → solver derives K from E, PR Validation checks • MID is positive integer • RO, E > 0 • 0 ≤ PR < 0.5 • 8 fields on card 1

Implementing the Generator

The function below is self-contained: it defines the field order, formats each value, handles the optional title line, and raises a clear error when a required field is missing. It uses only the standard library.

from typing import Any, Dict, List

# Canonical field order for *MAT_ELASTIC card 1.
MAT_ELASTIC_FIELDS: List[str] = ["MID", "RO", "E", "PR", "DA", "DB", "K", "VC"]

# Fields that must be present and non-empty.
MAT_ELASTIC_REQUIRED = {"MID", "RO", "E", "PR"}

def _fmt(value: Any) -> str:
    """Format a scalar for LS-DYNA comma-delimited input."""
    if value is None or value == "":
        return ""
    if isinstance(value, bool):
        raise TypeError("Boolean values are not valid LS-DYNA fields")
    if isinstance(value, int):
        return str(value)
    if isinstance(value, float):
        # Lowercase 'e', always a digit before the decimal point.
        s = f"{value:.10g}"
        if "e" in s:
            mantissa, exp = s.split("e")
            if "." not in mantissa:
                mantissa += ".0"
            s = f"{mantissa}e{int(exp)}"
        return s
    if isinstance(value, str):
        return value.strip()
    raise TypeError(f"Unsupported field type: {type(value).__name__}")

def generate_mat_elastic(params: Dict[str, Any]) -> str:
    """Emit a syntactically valid *MAT_ELASTIC card from a parameter dict."""
    missing = [k for k in MAT_ELASTIC_REQUIRED if params.get(k) in (None, "")]
    if missing:
        raise ValueError(f"Missing required *MAT_ELASTIC fields: {missing}")

    unknown = set(params) - set(MAT_ELASTIC_FIELDS) - {"TITLE"}
    if unknown:
        raise ValueError(f"Unknown *MAT_ELASTIC fields: {sorted(unknown)}")

    title = params.get("TITLE")
    keyword = "*MAT_ELASTIC_TITLE" if title else "*MAT_ELASTIC"

    lines = [keyword]
    if title:
        lines.append(str(title).strip())

    # Emit all eight fields; trim trailing empties only.
    values = [_fmt(params.get(f)) for f in MAT_ELASTIC_FIELDS]
    while values and values[-1] == "":
        values.pop()
    lines.append(",".join(values))
    return "\n".join(lines) + "\n"

Two design choices are worth calling out. First, _fmt normalizes floats through %.10g and then rewrites any exponent to lowercase with an explicit mantissa decimal, so 7.85e-09 becomes 7.85e-9 — both are accepted by LS-DYNA, but the second is what most preprocessors emit and what diff tools expect. Second, the generator trims only trailing empty fields. Interior empties (like K in the example) are preserved as consecutive commas, which is exactly what LS-DYNA needs to keep positional meaning.

Running the Generator on a Realistic Example

Using the dictionary from the diagram (AISI 4340 steel in mm–ms–kg units, so density is 7.85e-9 tonne/mm³ and Young's modulus is 210000 MPa):

params = {
    "MID": 101,
    "RO": 7.85e-9,
    "E": 210000.0,
    "PR": 0.3,
    "DA": 0.0,
    "DB": 0.0,
    "VC": 0.0,
    "TITLE": "Steel_AISI_4340",
}

print(generate_mat_elastic(params))

Expected output (this is what the function produces for the given input, not a solver run):

*MAT_ELASTIC_TITLE
Steel_AISI_4340
101,7.85e-9,210000.0,0.3,0.0,0.0,,0.0

Note the double comma between 0.0 and 0.0 — that is the empty K field. If you had passed "K": 0 instead, the output would contain ...,0.0,0.0,0,0.0, which LS-DYNA would interpret as a zero bulk modulus and reject or produce nonsense. This is the single most common bug when hand-rolling material generators.

Verification and Failure Symptoms

Before submitting the generated card to a solver, run three cheap checks:

  • Field count. Split the parameter line on commas; the count must be between 4 and 8, and every required field must be non-empty. A count of 3 or fewer means a required field was dropped.
  • Round-trip parse. Feed the output back through a simple splitter and confirm each value matches the input dictionary after normalization. This catches formatting drift.
  • Physical sanity. RO > 0, E > 0, 0 ≤ PR < 0.5. A Poisson's ratio of exactly 0.5 is incompressible and will cause hourglassing or solver termination in explicit runs.
If LS-DYNA reports *** Error reading material 101 or silently substitutes default density, the most likely causes are (a) a missing required field, (b) a comma replaced by whitespace, or (c) a value written in a locale that uses a comma as the decimal separator. The generator above avoids all three by construction, but if you extend it, keep those failure modes in mind.

Extending the Pattern

The same skeleton — a field-order list, a required-field set, a formatter, and a keyword/title switch — generalizes to every *MAT_ card. For *MAT_PLASTIC_KINEMATIC you add SIGY, ETAN, BETA, and the hardening flags; for *MAT_JOHNSON_COOK you add the equation-of-state reference and the five constitutive constants. The dictionary stays the same shape, and downstream automation never has to know which card is being emitted. That separation is what makes a 100-case material automation library maintainable rather than a pile of one-off scripts.