feat: adopt lumped mass and reduced-coordinate shell HHT

Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
This commit is contained in:
KOKO\Mimi
2026-10-13 16:57:19 +09:00
parent ede137c6b4
commit eaa5cbeb21
110 changed files with 15274 additions and 971 deletions
+64 -6
View File
@@ -1,4 +1,4 @@
"""Compare original Hex8 CSVs with official Solid HDF5; never launch a solver.
"""Compare original Hex8 CSV/TXT results with Solid HDF5; never launch a solver.
This entry point is intentionally restricted to reference/hexa's fixed original
models. Identity/schema checks precede finite checks and the common tolerance.
@@ -43,6 +43,7 @@ SOURCE_SHA256 = {
"hexa_dynnonlinear.inp": "1685f86f844e8020bf46e1747b949699ac9aff0d7570bd21ec95109399365ff9",
"hexa_dynnonlinear_U.csv": "f5832a24577f54838bb49dec86a0dc9668cf43e78953f718a9d3635e0f574226",
"hexa_freq.inp": "42cbcec2a8b738f8b6d2c1e13114b8840dccdde6fc3e6f8a43a3272e85d82347",
"hexa_freq.txt": "3d2dacbee13cd2cc8d3f7c1a38210611a72de41d5e31d169e636bf40e00d4370",
"hexa_linearstatic.inp": "67183909c5dc962a4d4b65ffb81f7bd65b72e89a5d015c8369a8e0226347a647",
"hexa_linearstatic_RF.csv": "3e04e71ec075ffef098cf4870d77f9c400557fccb375369da8bf67cf67c1e36b",
"hexa_linearstatic_S.csv": "8840a502b9d28a91e5685349301a56fed37d2ac62327b71304716b714db577e8",
@@ -293,8 +294,39 @@ def evaluate_family(identity, result, reference):
"diagnostic_code": "zero-reference-scale-nonzero-error" if not scale and not rows_passed else None}
def read_modal_reference(path: Path):
"""Read the supplied hexa_freq.txt Value=lambda, Freq=cycles/time format.
This artifact has no step field: its fixed case is bound to the sole Step-1
of the hash-checked hexa_freq.inp. Freq is finite auxiliary data; comparison
uses the printed Value directly, without frequency conversion or rounding.
"""
mapping = {}
for line in path.read_text(encoding="utf-8-sig").splitlines():
if not line.strip():
continue
match = re.fullmatch(
r"\s*Mode\s+([0-9]+):\s+Value\s*=\s*(\S+)\s+Freq\s*=\s*(\S+)\s+\(cycles/time\)\s*", line)
require(match is not None, "wrong-modal-reference-format", line)
mode = int(match[1])
require(mode in range(1, 11), "wrong-modal-mode", str(mode))
require(mode not in mapping, "duplicate-modal-mode", str(mode))
mapping[mode] = (match[2], match[3])
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
rows = []
for mode in range(1, 11):
value, frequency = (float(token) for token in mapping[mode])
require(math.isfinite(value) and math.isfinite(frequency), "nonfinite-modal-reference", str(mode))
require(value >= 0 and frequency >= 0, "invalid-modal-reference-value", str(mode))
rows.append({"step": STEP, "mode": mode, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": value})
require(all(a["value"] <= b["value"] for a, b in zip(rows, rows[1:])),
"wrong-modal-order", str(path))
return rows
def compare_modal(reference, result, quantity: str, unit: str):
"""Typed modal rows, not an invented format for the absent original CSV.
"""Compare explicitly typed modal rows using the common tolerance.
Each row explicitly supplies step, mode, quantity, unit_dimensions and value.
Callers must obtain those semantics from a genuine supplied artifact first.
@@ -316,7 +348,8 @@ def compare_modal(reference, result, quantity: str, unit: str):
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
inventories.append(mapping)
families = [evaluate_family(
{"case": "hexa_freq", "step": STEP, "mode": mode, "quantity": quantity,
{"identity": f"hexa_freq:{STEP}:mode-{mode}:{quantity}",
"case": "hexa_freq", "step": STEP, "mode": mode, "quantity": quantity,
"unit_dimensions": unit, "rows": [{"mode": mode, "component": quantity}]},
[inventories[1][mode]], [inventories[0][mode]]) for mode in range(1, 11)]
return {"families": families, "passed": all(family["passed"] for family in families)}
@@ -382,7 +415,7 @@ def read_results(path: Path, model):
for name, expected in {
"feature_id": "hex8-structural-analysis", "analysis_procedure": CASES[case],
"element_formulation": "FESA-Hex8", "integration_rule": "gauss-2x2x2",
"mass_formulation": "consistent-reference", "source_step_name": STEP,
"mass_formulation": "lumped-reference", "source_step_name": STEP,
"unit_system_label": "user-consistent-unspecified",
"coordinate_convention": "global-cartesian; reference-strain; current-cauchy-stress",
"component_order": "node-major; UX,UY,UZ",
@@ -635,6 +668,9 @@ def compare(case: str, input_path: Path, results_path: Path, reference_directory
quantities = ["U"] if case.startswith("hexa_dyn") else (["U", "RF", "S"] if case != "hexa_freq" else [])
paths = {"input": input_path, "results": results_path}
paths.update({quantity: reference_directory / f"{case}_{quantity}.csv" for quantity in quantities})
modal_path = reference_directory / "hexa_freq.txt"
if case == "hexa_freq" and modal_path.exists():
paths["Value"] = modal_path
for name, path in paths.items():
report["identities"][name] = {"path": lexical_absolute(path), "sha256": sha256(path)}
model = input_inventory(input_path, case)
@@ -660,6 +696,11 @@ def compare(case: str, input_path: Path, results_path: Path, reference_directory
"record_path": lexical_absolute(execution_record),
"record_sha256": sha256(execution_record)}
records = {}
modal_reference = None
if "Value" in paths:
require(report["identities"]["Value"]["sha256"] == SOURCE_SHA256["hexa_freq.txt"],
"reference-hash-mismatch", str(modal_path))
modal_reference = read_modal_reference(modal_path)
for quantity in quantities:
require(report["identities"][quantity]["sha256"] == SOURCE_SHA256[f"{case}_{quantity}.csv"],
"reference-hash-mismatch", str(paths[quantity]))
@@ -673,8 +714,25 @@ def compare(case: str, input_path: Path, results_path: Path, reference_directory
# Internal maps are useful to validation but unnecessary in the public report.
del report["precheck"]["result_node_map"], report["precheck"]["result_element_map"]
if case == "hexa_freq":
report["verdict"] = "not-run"
report["reason"] = "Original modal Value reference is absent; no format/quantity/unit assumption made."
if modal_reference is None:
report["verdict"] = "not-run"
report["reason"] = "Original modal Value reference is absent; no format/quantity/unit assumption made."
return report
actual = [{"step": STEP, "mode": int(row["mode_id"]), "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": float(row["eigenvalue"])}
for row in result["modal"]["modes"]]
modal = compare_modal(modal_reference, actual, "eigenvalue", "1/time^2")
report["precheck"]["reference_row_counts"]["Value"] = len(modal_reference)
report["modal_reference"] = {"format": "Mode n: Value = lambda Freq = f (cycles/time)",
"step_binding": "sole Step-1 of approved hexa_freq.inp",
"quantity": "eigenvalue", "unit_dimensions": "1/time^2",
"frequency_role": "finite auxiliary; no conversion"}
report["families"] = modal["families"]
for family in report["families"]:
family["reference_sha256"] = report["identities"]["Value"]["sha256"]
report["blocking_failures"] = [f["identity"] for f in modal["families"] if not f["passed"]]
report["blocking_passed"] = modal["passed"]
report["verdict"] = "passed" if modal["passed"] else "failed"
return report
families = []
for increment in sorted(result["frames"]):
+33 -3
View File
@@ -207,7 +207,7 @@ def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
metadata = file["metadata"]
for name, expected in {
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"mass_formulation": "consistent-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"mass_formulation": "nodal-lumped-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"initial_state": "zero-q-v-step-load-0+",
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
"source_step_name": STEP, "step_name": STEP, "unit_system_label": "consistent-input-units",
@@ -217,8 +217,19 @@ def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
source_identity = string_attribute(metadata, "source_input_identity")
require(source_identity.endswith(";content_identity=" + content_identity),
"wrong-input-content-identity", source_identity)
require("schema_version" in metadata.attrs, "missing-hdf5-attribute", "/metadata@schema_version")
version_attribute = metadata.attrs.get_id("schema_version")
require(version_attribute.shape == () and version_attribute.dtype == np.dtype("<u8"),
"wrong-hdf5-attribute-type", "/metadata@schema_version")
schema_version = int(metadata.attrs["schema_version"])
require(schema_version in (1, 2), "wrong-hdf5-attribute-value", "/metadata@schema_version")
if schema_version == 2:
string_attribute(metadata, "time_integration_formulation",
"reduced-coordinate-HHT-with-physical-rate-reconstruction")
elif "time_integration_formulation" in metadata.attrs:
string_attribute(metadata, "time_integration_formulation", "full-coordinate-HHT")
for name, dtype, value in (
("schema_version", "<u8", 1), ("increment_count", "<u8", 100),
("schema_version", "<u8", schema_version), ("increment_count", "<u8", 100),
("maximum_increments", "<u8", 1000), ("alpha", "<f8", -0.05),
("beta", "<f8", 0.275625), ("gamma", "<f8", 0.55),
("initial_increment", "<f8", INCREMENT), ("time_period", "<f8", 10.0),
@@ -279,11 +290,30 @@ def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
}.items():
string_attribute(displacement, name, value)
for name, dtype, value in (
("schema_version", "<u8", 1), ("frame_index", "<u8", frame),
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(displacement, name, dtype, value)
if schema_version == 2:
auxiliary = dataset(file, f"{frame_path}/{frame}/nodal/algorithmic_acceleration", (NODE_COUNT, 6))
require(auxiliary.dtype == np.dtype("<f8"), "wrong-auxiliary-type", str(frame))
for name, value in {
"component_names": "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6",
"component_unit_dimensions": "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2",
"coordinate_system": "fixed-initial-global-chart", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "full-chart-HHT-filter-combination-not-predictor-driver",
}.items():
string_attribute(auxiliary, name, value)
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(auxiliary, name, dtype, value)
require(np.isfinite(auxiliary[:]).all(), "nonfinite-auxiliary-value", str(frame))
values = displacement[:]
require(np.isfinite(values).all(), "nonfinite-result-value", str(frame))
if frame == 0: