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_TITLEwhen 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.
*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:
- Order is positional, not alphabetical. The dictionary may hold keys in any order; the generator must emit them in the canonical
*MAT_ELASTICorder. - Missing optional fields become empty strings, not zeros. Writing
0forKwould tell LS-DYNA the bulk modulus is literally zero, which is physically wrong. An empty field lets the solver compute it. - Numbers must be formatted without scientific notation surprises. LS-DYNA accepts
1.0e-9but not1E-09in every build; use lowercaseeand a leading digit before the decimal point.
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.
*** 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.