chore: remove retired workflow and historical material

This commit is contained in:
KOKO\Mimi
2026-10-17 22:39:47 +09:00
parent 4bd19f5bf0
commit 774b80099c
768 changed files with 0 additions and 54164 deletions
-856
View File
@@ -1,856 +0,0 @@
"""Independent comparison contracts; generated result fixtures are not source evidence."""
import csv
import importlib.util
import json
import math
import os
from pathlib import Path
import shutil
import subprocess
import sys
import h5py
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location(
"compare_hex8", ROOT / "scripts/verification/compare_hex8.py"
)
COMPARE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(COMPARE)
def family(actual, reference):
identities = [{"node_label": i + 1, "component": "U1"} for i in range(len(actual))]
return COMPARE.evaluate_family(
{"case": "hexa_linearstatic", "step": "Step-1", "increment": 10,
"quantity": "U", "unit_dimensions": "length", "rows": identities},
actual, reference,
)
def test_zero_reference_scale_requires_exact_zero():
report = family([0.0, 1e-300], [0.0, 0.0])
assert not report["passed"]
assert report["diagnostic_code"] == "zero-reference-scale-nonzero-error"
def test_row_pass_does_not_override_one_percent_rms():
report = family([1.02] * 10, [1.0] * 10)
assert report["rows_passed"]
assert not report["rms_passed"]
assert not report["passed"]
def test_near_zero_band_uses_reference_scale_only():
report = family([100.0, 1.0], [100.0, 0.0])
assert report["passed"]
assert report["rows"][1]["branch"] == "near-zero"
assert not family([100.0, 1.0001], [100.0, 0.0])["passed"]
def test_nonfinite_family_fails_before_tolerance():
with pytest.raises(COMPARE.ComparisonError, match="finite"):
family([float("nan")], [1.0])
def test_header_strip_collision_rejected_before_bad_numeric(tmp_path):
path = tmp_path / "collision.csv"
path.write_text("Node Label, Node Label\n1,NaN\n", encoding="utf-8")
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_linearstatic", "U", {})
assert error.value.code == "duplicate-reference-header"
def test_modal_quantity_and_units_are_explicit():
rows = [{"step": "Step-1", "mode": i, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": float(i)} for i in range(1, 11)]
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.compare_modal(rows, rows, "frequency", "1/time")
assert error.value.code == "wrong-modal-quantity"
def test_modal_singletons_use_common_one_percent_rms():
reference = [{"step": "Step-1", "mode": i, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": float(i)} for i in range(1, 11)]
actual = [dict(row, value=row["value"] * 1.02) for row in reference]
report = COMPARE.compare_modal(reference, actual, "eigenvalue", "1/time^2")
assert not report["passed"]
assert len(report["families"]) == 10
assert all(row["rows_passed"] and not row["rms_passed"] for row in report["families"])
# These schema declarations are independent of the comparator's declarations.
TEXT = h5py.string_dtype(encoding="utf-8")
REFERENCE = ROOT / "reference/hexa"
def write_strings(obj, attributes):
for name, value in attributes.items():
obj.attrs.create(name, value, dtype=TEXT)
def compound(group, name, schema, rows):
data = np.empty(len(rows), dtype=np.dtype(schema))
for i, row in enumerate(rows):
data[i] = row
return group.create_dataset(name, data=data)
@pytest.fixture(scope="module")
def source_models():
return {case: COMPARE.input_inventory(REFERENCE / f"{case}.inp", case)
for case in ("hexa_linearstatic", "hexa_nonlinearstatic", "hexa_dynlinear", "hexa_dynnonlinear", "hexa_freq")}
def source_rows(case, quantity):
with (REFERENCE / f"{case}_{quantity}.csv").open(newline="", encoding="utf-8") as stream:
reader = csv.reader(stream)
headers = [value.strip() for value in next(reader)]
return [dict(zip(headers, [value.strip() for value in row])) for row in reader]
def make_result(path, model):
"""Build an original-sized schema fixture, not a validated physical solution."""
case = model["case"]
dynamic = case in ("hexa_dynlinear", "hexa_dynnonlinear")
nonlinear = case in ("hexa_nonlinearstatic", "hexa_dynnonlinear")
frequency = case == "hexa_freq"
reference_values = {}
if not frequency:
for quantity in (["U"] if dynamic else ["U", "RF", "S"]):
table_rows = {}
for row in source_rows(case, quantity):
increment = int(row["Frame"].split(":")[0].split()[-1])
entity = int(row["Element Label" if quantity == "S" else "Node Label"])
point = int(row["IntPt"]) if quantity == "S" else 0
names = ("S11", "S22", "S33", "S12", "S13", "S23") if quantity == "S" else tuple(f"{quantity}{c}" for c in (1, 2, 3))
table_rows[increment, entity, point] = [float(row[f"{quantity}-{name}"]) for name in names]
reference_values[quantity] = table_rows
with h5py.File(path, "w") as file:
metadata = file.create_group("metadata")
metadata.attrs.create("schema_version", np.uint64(1), dtype="<u8")
write_strings(metadata, {
"feature_id": "hex8-structural-analysis", "analysis_procedure": {
"hexa_linearstatic": "linear-static", "hexa_nonlinearstatic": "nonlinear-static-full-newton",
"hexa_dynlinear": "hht-linear-dynamics", "hexa_dynnonlinear": "hht-nonlinear-dynamics",
"hexa_freq": "linear-frequency"}[case],
"element_formulation": "FESA-Hex8", "integration_rule": "gauss-2x2x2",
"mass_formulation": "lumped-reference", "source_step_name": "Step-1",
"source_input_identity": f"path={model['source_path']};content_identity={model['content_identity']}",
"unit_system_label": "user-consistent-unspecified",
"coordinate_convention": "global-cartesian; reference-strain; current-cauchy-stress",
"component_order": "node-major; UX,UY,UZ", "solver_version": "fixture-not-source-evidence",
})
group = file.create_group("model")
compound(group, "nodes", [("internal_node_id", "<u8"), ("instance_name", TEXT),
("source_label", TEXT), ("coordinates", "<f8", (3,))],
[(i, "Part-1-1", str(label), coords) for i, (label, coords) in enumerate(model["nodes"].items())])
compound(group, "elements", [("internal_element_id", "<u8"), ("instance_name", TEXT),
("source_label", TEXT), ("source_element_type", TEXT),
("internal_formulation", TEXT), ("node_internal_ids", "<u8", (8,)),
("solid_section_internal_id", "<u8"), ("material_internal_id", "<u8")],
[(i, "Part-1-1", str(label), "C3D8", "FESA-Hex8", tuple(n - 1 for n in nodes), 0, 0)
for i, (label, nodes) in enumerate(model["elements"].items())])
solids = group.create_group("solid")
compound(solids, "materials", [("internal_material_id", "<u8"), ("name", TEXT),
("youngs_modulus", "<f8"), ("poisson_ratio", "<f8"),
("density_present", "u1"), ("density", "<f8")],
[(0, "Material-1", 2.1e11, .3, 1, 7850.)])
compound(solids, "sections", [("internal_section_id", "<u8"), ("source_file", TEXT),
("source_line", "<u8"), ("source_elset", TEXT), ("material_internal_id", "<u8")],
[(0, model["source_path"], model["section_line"], "Set-1", 0)])
point_signs = ((-1, -1, -1), (1, -1, -1), (-1, 1, -1), (1, 1, -1),
(-1, -1, 1), (1, -1, 1), (-1, 1, 1), (1, 1, 1))
node_signs = ((-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1))
points = []
for i, (_, connectivity) in enumerate(model["elements"].items()):
coords = np.array([model["nodes"][n] for n in connectivity])
for p, signs in enumerate(point_signs, 1):
natural = np.array(signs) / math.sqrt(3)
shape = np.array([math.prod(1 + sign * x for sign, x in zip(node, natural)) / 8
for node in node_signs])
points.append((i, p, natural, np.sum(shape[:, None] * coords, axis=0)))
compound(group, "solid_integration_points", [("internal_element_id", "<u8"), ("point_id", "u1"),
("natural_coordinates", "<f8", (3,)),
("reference_coordinates", "<f8", (3,))], points)
mask = group.create_dataset("nodal_constraint_mask", data=np.array(
[[(node, c) in model["constrained"] for c in (1, 2, 3)] for node in model["nodes"]], dtype="u1"))
write_strings(mask, {"component_names": "UX,UY,UZ"})
step = file.create_group("steps/Step-1")
if frequency:
eigen = step.create_group("eigen")
modes = compound(eigen, "modes", [("mode_id", "<u8"), ("eigenvalue", "<f8"),
("classification", "u1"), ("cluster_id", "<u8")],
[(m, float(m), 1, m) for m in range(1, 11)])
frequencies = compound(eigen, "frequencies", [("mode_id", "<u8"), ("frequency", "<f8")],
[(m, math.sqrt(m) / (2 * math.pi)) for m in range(1, 11)])
for obj, name, units in ((modes, "eigenvalue", "1/time^2"), (frequencies, "frequency", "1/time")):
write_strings(obj, {"component_names": name, "unit_dimensions": units,
"location": "global", "coordinate_system": "global-cartesian"})
vectors = eigen.create_dataset("vectors", data=np.zeros((10, 442, 3), dtype="<f8"))
write_strings(vectors, {"component_names": "UX,UY,UZ", "unit_dimensions": "mass-normalized",
"location": "nodal", "coordinate_system": "global-cartesian",
"normalization": "phi^T*M*phi=1"})
verification = eigen.create_group("verification")
compound(verification, "pairs", [("sorted_rank", "<u8"), ("solver_column", "<u8"),
("eigenvalue", "<f8"), ("modal_mass", "<f8"),
("rayleigh_quotient", "<f8"), ("eta_r", "<f8"),
("eta_norm", "<f8"), ("eta_energy", "<f8")],
[(m, m - 1, float(m), 1., float(m), 0., 0., 0.) for m in range(1, 11)])
verification.create_dataset("eta_gram", data=0., dtype="<f8")
verification.create_dataset("max_eta_orth", data=0., dtype="<f8")
return
count = 101 if dynamic else (10 if nonlinear else 1)
if dynamic or nonlinear:
compound(step, "accepted_frames", [("frame_index", "<u8"), ("increment_index", "<u8"),
("time" if dynamic else "load_factor", "<f8"),
("iteration_count", "<u8"),
("is_initial" if dynamic else "is_factor_one", "u1")],
[(frame, frame if dynamic else frame + 1, (frame if dynamic else frame + 1) * .1,
int(not (dynamic and frame == 0)), int(frame == (0 if dynamic else 9))) for frame in range(count)])
for frame in range(count):
increment = frame if dynamic else (frame + 1 if nonlinear else 10)
coordinate = increment * .1 if dynamic or nonlinear else 1.
frame_group = step.create_group(f"frames/{frame}")
definitions = {
"nodal/displacement": ((442, 3), "U1,U2,U3", "length", {}),
"nodal/reaction": ((442, 3), "RF1,RF2,RF3", "force", {}),
"nodal/residual": ((442, 3), "R1,R2,R3", "force", {}),
"solid/stress": ((300, 8, 6), "S11,S22,S33,S12,S13,S23", "force/length^2", {"stress_measure": "cauchy"}),
"solid/strain": ((300, 8, 6), "E11,E22,E33,E12,E13,E23", "dimensionless",
{"strain_measure": "green-lagrange" if nonlinear else "infinitesimal", "shear_convention": "tensor"}),
"global/strain_energy": ((), "scalar", "force*length", {"definition": "sum(W*detJ0)" if nonlinear else "0.5*u^T*K0*u"}),
}
if dynamic:
definitions.update({
"nodal/velocity": ((442, 3), "V1,V2,V3", "length/time", {}),
"nodal/acceleration": ((442, 3), "A1,A2,A3", "length/time^2", {"acceleration_kind": "physical"}),
"nodal/algorithmic_acceleration": ((442, 3), "A1,A2,A3", "length/time^2", {"acceleration_kind": "algorithmic"}),
"global/kinetic_energy": ((), "scalar", "force*length", {"definition": "0.5*v^T*M*v"}),
"global/external_work": ((), "scalar", "force*length", {"definition": "sum(0.5*(F_old+F_new)*delta_u)"}),
"global/energy_balance_error": ((), "scalar", "force*length", {"definition": "T+U-T0-U0-Wext"}),
"global/linear_momentum": ((3,), "P1,P2,P3", "mass*length/time", {}),
"global/angular_momentum": ((3,), "L1,L2,L3", "mass*length^2/time", {"origin": "global0"}),
})
for key, (shape, names, units, extra) in definitions.items():
values = np.zeros(shape, dtype="<f8")
quantity = {"nodal/displacement": "U", "nodal/reaction": "RF", "solid/stress": "S"}.get(key)
if quantity in reference_values and increment in (range(101) if dynamic else (10,)):
for (n, entity, point), components in reference_values[quantity].items():
if n == increment:
values[(entity - 1, point - 1) if quantity == "S" else entity - 1] = components
obj = frame_group.create_dataset(key, data=values)
write_strings(obj, {"component_names": names, "unit_dimensions": units, "step_name": "Step-1",
"coordinate_system": "global-cartesian", "location": {
"nodal": "nodal", "solid": "integration-point", "global": "global"}[key.split('/')[0]], **extra})
obj.attrs.create("frame_index", np.uint64(frame), dtype="<u8")
obj.attrs.create("increment_index", np.uint64(increment), dtype="<u8")
obj.attrs.create("time" if dynamic else "load_factor", coordinate, dtype="<f8")
@pytest.fixture(scope="module")
def result_fixtures(tmp_path_factory, source_models):
directory = tmp_path_factory.mktemp("hex8-comparison-fixtures")
paths = {}
for case, model in source_models.items():
path = directory / f"{case}.h5"
make_result(path, model)
paths[case] = path
return paths
def copy_result(tmp_path, result_fixtures, case="hexa_linearstatic"):
path = tmp_path / "mutated.h5"
shutil.copyfile(result_fixtures[case], path)
return path
def compare_fixture(path, case="hexa_linearstatic", execution=None):
return COMPARE.compare(case, REFERENCE / f"{case}.inp", path, REFERENCE, execution)
def mutate_csv(tmp_path, case, quantity, mutation):
rows = source_rows(case, quantity)
headers = list(rows[0])
mutation(headers, rows)
path = tmp_path / "mutated.csv"
with path.open("w", newline="", encoding="utf-8") as stream:
writer = csv.DictWriter(stream, fieldnames=headers)
writer.writeheader()
writer.writerows(rows)
return path
@pytest.mark.parametrize("case", ["hexa_linearstatic", "hexa_nonlinearstatic", "hexa_dynlinear", "hexa_dynnonlinear"])
def test_all_original_sized_schema_fixtures_compare_by_identity(case, result_fixtures):
report = compare_fixture(result_fixtures[case], case)
assert report["precheck"]["passed"], report["precheck"]
assert report["verdict"] == "passed"
assert report["execution"]["verified"] is False
assert len(report["families"]) == (101 if case.startswith("hexa_dyn") else 3)
if not case.startswith("hexa_dyn"):
assert [family["row_count"] for family in report["families"]] == [1326, 51, 14400]
assert report["precheck"]["reference_row_counts"]["RF"] == 442
else:
assert [family["increment"] for family in report["families"]] == list(range(101))
assert all(family["row_count"] == 1326 for family in report["families"])
assert report["families"][0]["reference_scale"] == 0.
def test_static_final_factor_maps_to_increment10_not_storage_frame(result_fixtures):
report = compare_fixture(result_fixtures["hexa_nonlinearstatic"], "hexa_nonlinearstatic")
assert {family["increment"] for family in report["families"]} == {10}
assert all(family["passed"] for family in report["families"])
@pytest.mark.parametrize("quantity", ["U", "RF", "S"])
def test_csv_order_does_not_supply_identity(tmp_path, source_models, quantity):
path = mutate_csv(tmp_path, "hexa_linearstatic", quantity, lambda _, rows: rows.reverse())
rows = COMPARE.read_reference(path, "hexa_linearstatic", quantity, source_models["hexa_linearstatic"])
original = COMPARE.read_reference(REFERENCE / f"hexa_linearstatic_{quantity}.csv", "hexa_linearstatic", quantity, source_models["hexa_linearstatic"])
assert COMPARE.reference_values(rows, quantity, source_models["hexa_linearstatic"]) == COMPARE.reference_values(original, quantity, source_models["hexa_linearstatic"])
@pytest.mark.parametrize("field,value,code", [
("Node Label", "443", "extra-reference-entity"), ("Part Instance Name", "OTHER", "wrong-reference-instance"),
("Step", "Step-2", "wrong-reference-step"), ("ODB Name", "hexa_nonlinearstatic.odb", "wrong-reference-case"),
("Frame", "Increment 9: Step Time = 1.000", "wrong-reference-frame-time"),
("Frame", "Increment 10: Step Time = 0.999", "wrong-reference-frame-time"),
])
def test_reference_identity_errors_precede_nonfinite(tmp_path, source_models, field, value, code):
def mutation(_, rows):
rows[0]["U-U1"] = "NaN"
rows[-1][field] = value
path = mutate_csv(tmp_path, "hexa_linearstatic", "U", mutation)
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_linearstatic", "U", source_models["hexa_linearstatic"])
assert error.value.code == code
def test_rf_entire_inventory_precedes_constraint_projection(tmp_path, source_models):
def mutation(_, rows):
assert int(rows[0]["Node Label"]) == 1 # A free node, absent from projected RF family.
rows.pop(0)
path = mutate_csv(tmp_path, "hexa_linearstatic", "RF", mutation)
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_linearstatic", "RF", source_models["hexa_linearstatic"])
assert error.value.code == "reference-inventory"
def test_duplicate_reference_identity_precedes_bad_numeric(tmp_path, source_models):
def mutation(_, rows):
rows[0]["RF-RF1"] = "NaN"
rows[-1]["Node Label"] = rows[0]["Node Label"]
path = mutate_csv(tmp_path, "hexa_linearstatic", "RF", mutation)
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_linearstatic", "RF", source_models["hexa_linearstatic"])
assert error.value.code == "duplicate-reference-identity"
@pytest.mark.parametrize("point", ["0", "9", "bad"])
def test_invalid_stress_point_is_never_rematched(tmp_path, source_models, point):
path = mutate_csv(tmp_path, "hexa_linearstatic", "S", lambda _, rows: rows[0].update(IntPt=point))
with pytest.raises(COMPARE.ComparisonError):
COMPARE.read_reference(path, "hexa_linearstatic", "S", source_models["hexa_linearstatic"])
def test_stress_point_order_coordinates_are_auxiliary_not_identity(tmp_path, source_models):
path = mutate_csv(tmp_path, "hexa_linearstatic", "S", lambda _, rows: rows[2].update(
X=rows[3]["X"], Y=rows[3]["Y"], Z=rows[3]["Z"]))
rows = COMPARE.read_reference(path, "hexa_linearstatic", "S", source_models["hexa_linearstatic"])
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.reference_values(rows, "S", source_models["hexa_linearstatic"])
assert error.value.code == "reference-coordinate-convention"
@pytest.mark.parametrize("header_mutation", ["missing", "extra"])
def test_wrong_component_columns_rejected(tmp_path, source_models, header_mutation):
def mutation(headers, rows):
if header_mutation == "missing":
headers.remove("U-U3")
for row in rows:
del row["U-U3"]
else:
headers.append("U-UR1")
for row in rows:
row["U-UR1"] = "0"
path = mutate_csv(tmp_path, "hexa_linearstatic", "U", mutation)
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_linearstatic", "U", source_models["hexa_linearstatic"])
assert error.value.code == "wrong-reference-columns"
def test_dynamic_decimal_time_requires_exact_integer_frame(tmp_path, source_models):
path = mutate_csv(tmp_path, "hexa_dynlinear", "U", lambda _, rows: rows[442].update(
Frame="Increment 1: Step Time = 0.1000000001"))
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_reference(path, "hexa_dynlinear", "U", source_models["hexa_dynlinear"])
assert error.value.code == "wrong-reference-frame-time"
def test_nonfinite_free_reference_rf_is_not_discarded(tmp_path, source_models):
path = mutate_csv(tmp_path, "hexa_linearstatic", "RF", lambda _, rows: rows[0].update({"RF-RF1": "NaN"}))
rows = COMPARE.read_reference(path, "hexa_linearstatic", "RF", source_models["hexa_linearstatic"])
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.reference_values(rows, "RF", source_models["hexa_linearstatic"])
assert error.value.code == "nonfinite-reference-value"
def test_inclusive_row_and_rms_boundaries_are_independent():
report = family([21.] + [20.] * 99, [20.] * 100)
assert report["passed"] and report["rows"][0]["relative_error"] == .05
assert not family([21.00001] + [20.] * 99, [20.] * 100)["rows_passed"]
assert family([101.], [100.])["passed"]
assert not family([101.00001], [100.])["rms_passed"]
assert family([100., 2.], [100., 1.])["rows"][1]["branch"] == "near-zero"
@pytest.mark.parametrize("actual,reference", [([1e308], [-1e308]), ([float('inf')], [1.])])
def test_overflow_cannot_create_passing_metric(actual, reference):
with pytest.raises(COMPARE.ComparisonError):
family(actual, reference)
def test_wrong_result_node_beats_nonfinite_displacement(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
nodes = file["model/nodes"][:]
nodes[-1]["source_label"] = b"1"
file["model/nodes"][:] = nodes
file["steps/Step-1/frames/0/nodal/displacement"][0, 0] = np.nan
report = compare_fixture(path)
assert report["precheck"]["code"] == "result-node-inventory"
assert report["families"] == []
@pytest.mark.parametrize("attribute,value,code", [
("source_input_identity", "path=wrong;content_identity=fnv1a64:0000000000000000", "wrong-result-attribute"),
("analysis_procedure", "hht-linear-dynamics", "wrong-result-attribute"),
("mass_formulation", "consistent-reference", "wrong-result-attribute"),
("component_order", "UX,UY,UZ,URX,URY,URZ", "wrong-result-attribute"),
])
def test_metadata_identity_rejects_other_model(tmp_path, result_fixtures, attribute, value, code):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
file["metadata"].attrs.modify(attribute, value)
assert compare_fixture(path)["precheck"]["code"] == code
@pytest.mark.parametrize("attribute,value", [("component_names", "U1,U3,U2"), ("unit_dimensions", "force"),
("location", "integration-point"), ("coordinate_system", "local")])
def test_result_quantity_semantics_must_match(tmp_path, result_fixtures, attribute, value):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
file["steps/Step-1/frames/0/nodal/displacement"].attrs.modify(attribute, value)
assert compare_fixture(path)["precheck"]["code"] == "wrong-result-attribute"
@pytest.mark.parametrize("shape,dtype,code", [((442, 6), "<f8", "wrong-result-shape"),
((442, 3), "<f4", "wrong-result-value-type"),
((442, 3), ">f8", "wrong-result-value-type")])
def test_wrong_shape_or_dtype_is_not_sliced_into_supported_components(tmp_path, result_fixtures, shape, dtype, code):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
key = "steps/Step-1/frames/0/nodal/displacement"
del file[key]
file.create_dataset(key, shape=shape, dtype=dtype)
assert compare_fixture(path)["precheck"]["code"] == code
def test_signed_ids_and_fixed_ascii_are_not_official_compound_schema(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
old = file["model/nodes"][:]
del file["model/nodes"]
compound(file["model"], "nodes", [("internal_node_id", "<i8"), ("instance_name", "S16"),
("source_label", "S16"), ("coordinates", "<f8", (3,))], list(old))
assert compare_fixture(path)["precheck"]["code"] == "wrong-result-table-schema"
@pytest.mark.parametrize("target", ["missing", "extra", "noncanonical"])
def test_dynamic_frame_inventory_is_exact(tmp_path, result_fixtures, target):
path = copy_result(tmp_path, result_fixtures, "hexa_dynlinear")
with h5py.File(path, "r+") as file:
root = file["steps/Step-1/frames"]
if target == "missing":
del root["100"]
elif target == "extra":
root.create_group("101")
else:
root.move("1", "01")
assert compare_fixture(path, "hexa_dynlinear")["precheck"]["code"] == "result-frame-inventory"
def test_dynamic_binary_schedule_not_nearest_time(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures, "hexa_dynlinear")
with h5py.File(path, "r+") as file:
rows = file["steps/Step-1/accepted_frames"][:]
rows[3]["time"] = .3 # Required binary64 3*.1 differs from decimal-literal .3.
file["steps/Step-1/accepted_frames"][:] = rows
assert compare_fixture(path, "hexa_dynlinear")["precheck"]["code"] == "wrong-result-frame-schedule"
def test_result_point_id_mismatch_rejected_before_nan_stress(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
rows = file["model/solid_integration_points"][:]
rows[2]["point_id"] = 4
file["model/solid_integration_points"][:] = rows
file["steps/Step-1/frames/0/solid/stress"][0, 0, 0] = np.nan
assert compare_fixture(path)["precheck"]["code"] == "wrong-result-point-identity"
def test_result_free_reaction_is_not_free_residual(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
file["steps/Step-1/frames/0/nodal/residual"][0, 0] = 1234.
assert compare_fixture(path)["verdict"] == "passed"
with h5py.File(path, "r+") as file:
file["steps/Step-1/frames/0/nodal/reaction"][0, 0] = 1234.
assert compare_fixture(path)["precheck"]["code"] == "nonzero-free-result-reaction"
def test_nonfinite_uncompared_quantity_is_schema_failure(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
file["steps/Step-1/frames/0/global/strain_energy"][()] = np.inf
assert compare_fixture(path)["precheck"]["code"] == "nonfinite-result-value"
def test_source_copy_must_be_byte_identical(tmp_path, result_fixtures):
source = tmp_path / "hexa_linearstatic.inp"
source.write_bytes((REFERENCE / "hexa_linearstatic.inp").read_bytes().replace(b"\r\n", b"\n"))
report = COMPARE.compare("hexa_linearstatic", source, result_fixtures["hexa_linearstatic"])
assert report["precheck"]["code"] == "source-input-hash-mismatch"
def test_report_is_deterministic_and_keeps_family_boundaries(result_fixtures):
first = compare_fixture(result_fixtures["hexa_linearstatic"])
second = compare_fixture(result_fixtures["hexa_linearstatic"])
assert json.dumps(first, sort_keys=True, allow_nan=False) == json.dumps(second, sort_keys=True, allow_nan=False)
assert [row["quantity"] for row in first["families"]] == ["U", "RF", "S"]
assert len({row["reference_sha256"] for row in first["families"]}) == 3
def test_modal_original_absence_is_not_pass(tmp_path, result_fixtures):
report = COMPARE.compare("hexa_freq", REFERENCE / "hexa_freq.inp",
result_fixtures["hexa_freq"], tmp_path)
assert report["precheck"]["passed"], report["precheck"]
assert report["verdict"] == "not-run" and not report["blocking_passed"]
assert report["families"] == []
def test_supplied_modal_values_are_compared_not_reported_missing(result_fixtures):
report = compare_fixture(result_fixtures["hexa_freq"], "hexa_freq")
assert report["precheck"]["passed"], report["precheck"]
# The artificial eigenvalues 1..10 must fail against genuine source Values.
assert report["verdict"] == "failed"
assert len(report["blocking_failures"]) == 10
assert report["precheck"]["reference_row_counts"]["Value"] == 10
assert [f["mode"] for f in report["families"]] == list(range(1, 11))
assert all(f["quantity"] == "eigenvalue" for f in report["families"])
def test_modal_eigenvalue_matches_pass_with_ten_independent_families(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures, "hexa_freq")
reference = COMPARE.read_modal_reference(REFERENCE / "hexa_freq.txt")
with h5py.File(path, "r+") as file:
eigen = file["steps/Step-1/eigen"]
for name, field in (("modes", "eigenvalue"), ("verification/pairs", "eigenvalue"),
("frequencies", "frequency")):
rows = eigen[name][:]
rows[field] = [math.sqrt(row["value"]) / (2 * math.pi) if field == "frequency"
else row["value"] for row in reference]
eigen[name][:] = rows
first = compare_fixture(path, "hexa_freq")
assert first["verdict"] == "passed", first
assert first["blocking_passed"] and first["blocking_failures"] == []
assert len({f["identity"] for f in first["families"]}) == 10
assert all(f["relative_rms"] == 0 and f["row_count"] == 1 for f in first["families"])
assert first == compare_fixture(path, "hexa_freq")
def test_modal_text_reads_value_not_cycles_per_time(tmp_path):
original = REFERENCE / "hexa_freq.txt"
rows = COMPARE.read_modal_reference(original)
assert rows[0] == {"step": "Step-1", "mode": 1, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": 496.28}
assert rows[-1]["value"] == 449260.0
reversed_path = tmp_path / "reversed.txt"
reversed_path.write_text("\n".join(reversed(original.read_text().splitlines())), encoding="utf-8")
assert COMPARE.read_modal_reference(reversed_path) == rows
@pytest.mark.parametrize("mutation,code", [
("missing", "modal-mode-inventory"), ("duplicate", "duplicate-modal-mode"),
("extra", "wrong-modal-mode"), ("unit", "wrong-modal-reference-format"),
("quantity", "wrong-modal-reference-format"), ("nan", "nonfinite-modal-reference"),
("negative", "invalid-modal-reference-value"), ("order", "wrong-modal-order"),
])
def test_modal_text_rejects_invalid_source(tmp_path, mutation, code):
lines = [line for line in (REFERENCE / "hexa_freq.txt").read_text().splitlines() if line.strip()]
if mutation == "missing":
lines.pop()
elif mutation == "duplicate":
lines.append(lines[0])
elif mutation == "extra":
lines.append("Mode 11: Value = 500000 Freq = 112.54 (cycles/time)")
elif mutation == "unit":
lines[0] = lines[0].replace("cycles/time", "Hz")
elif mutation == "quantity":
lines[0] = lines[0].replace("Value", "Frequency")
elif mutation == "nan":
lines[0] = lines[0].replace("496.28", "NaN")
elif mutation == "negative":
lines[0] = lines[0].replace("3.5456", "-3.5456")
else:
lines[0] = lines[0].replace("496.28", "900000")
path = tmp_path / "invalid.txt"
path.write_text("\n".join(lines), encoding="utf-8")
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.read_modal_reference(path)
assert error.value.code == code
def test_modified_modal_reference_is_precheck_failure(tmp_path, result_fixtures):
(tmp_path / "hexa_freq.txt").write_bytes((REFERENCE / "hexa_freq.txt").read_bytes() + b"\n")
report = COMPARE.compare("hexa_freq", REFERENCE / "hexa_freq.inp",
result_fixtures["hexa_freq"], tmp_path)
assert report["verdict"] == "failed"
assert report["precheck"]["code"] == "reference-hash-mismatch"
@pytest.mark.parametrize("mutation,code", [("duplicate", "duplicate-modal-mode"), ("missing", "modal-mode-inventory"),
("extra", "wrong-modal-mode"), ("unit", "wrong-modal-unit"),
("step", "wrong-modal-step")])
def test_modal_identity_before_nonfinite(mutation, code):
rows = [{"step": "Step-1", "mode": i, "quantity": "frequency", "unit_dimensions": "1/time",
"value": float(i)} for i in range(1, 11)]
actual = [dict(row) for row in rows]
actual[0]["value"] = np.nan
if mutation == "duplicate":
actual[-1]["mode"] = 1
elif mutation == "missing":
actual.pop()
elif mutation == "extra":
actual.append(dict(rows[-1], mode=11))
elif mutation == "unit":
actual[-1]["unit_dimensions"] = "Hz_assumed_SI"
else:
actual[-1]["step"] = "Step-2"
with pytest.raises(COMPARE.ComparisonError) as error:
COMPARE.compare_modal(rows, actual, "frequency", "1/time")
assert error.value.code == code
def test_closed_execution_record_hashes_are_checked(tmp_path, result_fixtures):
path = result_fixtures["hexa_linearstatic"]
record = tmp_path / "native.json"
data = {"case": "hexa_linearstatic", "native_exit": 0, "source_unchanged": True,
"scratch_unchanged": True, "result_exists": True, "command": ["fesa.exe", str(REFERENCE / "hexa_linearstatic.inp")],
"original_sha256": COMPARE.sha256(REFERENCE / "hexa_linearstatic.inp"),
"scratch_sha256": COMPARE.sha256(REFERENCE / "hexa_linearstatic.inp"),
"result_sha256": COMPARE.sha256(path)}
record.write_text(json.dumps(data), encoding="utf-8")
assert compare_fixture(path, execution=record)["execution"]["verified"]
data["native_exit"] = 1
record.write_text(json.dumps(data), encoding="utf-8")
assert compare_fixture(path, execution=record)["precheck"]["code"] == "invalid-execution-evidence"
data["native_exit"], data["result_sha256"] = 0, "0" * 64
record.write_text(json.dumps(data), encoding="utf-8")
assert compare_fixture(path, execution=record)["precheck"]["code"] == "invalid-execution-evidence"
def test_cli_protects_reference_and_has_deterministic_exit(tmp_path, result_fixtures):
original_hash = COMPARE.sha256(REFERENCE / "hexa_linearstatic_U.csv")
command = [sys.executable, str(ROOT / "scripts/verification/compare_hex8.py"),
"--case", "hexa_linearstatic", "--input", str(REFERENCE / "hexa_linearstatic.inp"),
"--results", str(result_fixtures["hexa_linearstatic"])]
denied = subprocess.run(command + ["--report", str(REFERENCE / "hexa_linearstatic_U.csv")], capture_output=True, text=True)
assert denied.returncode == 2
assert COMPARE.sha256(REFERENCE / "hexa_linearstatic_U.csv") == original_hash
report_path = tmp_path / "report.json"
completed = subprocess.run(command + ["--report", str(report_path)], capture_output=True, text=True)
assert completed.returncode == 0, completed.stderr
assert json.loads(report_path.read_text())["verdict"] == "passed"
def test_result_projection_joins_source_identities_not_storage_order(tmp_path, result_fixtures):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
nodes = file["model/nodes"][:][::-1]
nodes["internal_node_id"] = np.arange(442, dtype="<u8")
file["model/nodes"][:] = nodes
for name in ("displacement", "reaction", "residual"):
obj = file[f"steps/Step-1/frames/0/nodal/{name}"]
obj[:] = obj[:][::-1]
mask = file["model/nodal_constraint_mask"]
mask[:] = mask[:][::-1]
elements = file["model/elements"][:][::-1]
elements["internal_element_id"] = np.arange(300, dtype="<u8")
elements["node_internal_ids"] = 441 - elements["node_internal_ids"]
file["model/elements"][:] = elements
points = file["model/solid_integration_points"][:].reshape(300, 8)[::-1].reshape(2400)
points["internal_element_id"] = np.repeat(np.arange(300, dtype="<u8"), 8)
file["model/solid_integration_points"][:] = points
for name in ("stress", "strain"):
obj = file[f"steps/Step-1/frames/0/solid/{name}"]
obj[:] = obj[:][::-1]
report = compare_fixture(path)
assert report["verdict"] == "passed", report["precheck"]
@pytest.mark.parametrize("mutation,code", [
("point-natural", "result-coordinate-convention"), ("point-reference", "result-coordinate-convention"),
("connectivity", "wrong-result-element-connectivity"), ("mask", "wrong-result-constraint-identity"),
("node-coordinate", "wrong-result-coordinate"), ("material", "wrong-result-material-value"),
("section", "wrong-result-section-identity"),
])
def test_model_geometry_and_properties_match_frozen_input(tmp_path, result_fixtures, mutation, code):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
if mutation.startswith("point-"):
obj = file["model/solid_integration_points"]
rows = obj[:]
field = "natural_coordinates" if mutation == "point-natural" else "reference_coordinates"
rows[0][field][0] += .001
obj[:] = rows
elif mutation == "connectivity":
obj = file["model/elements"]
rows = obj[:]
rows[0]["node_internal_ids"][[0, 1]] = rows[0]["node_internal_ids"][[1, 0]]
obj[:] = rows
elif mutation == "mask":
file["model/nodal_constraint_mask"][0, 0] = 1
elif mutation == "node-coordinate":
obj = file["model/nodes"]
rows = obj[:]
rows[0]["coordinates"][0] = .001
obj[:] = rows
elif mutation == "material":
obj = file["model/solid/materials"]
rows = obj[:]
rows[0]["youngs_modulus"] *= 2
obj[:] = rows
else:
obj = file["model/solid/sections"]
rows = obj[:]
rows[0]["source_line"] += 1
obj[:] = rows
assert compare_fixture(path)["precheck"]["code"] == code
@pytest.mark.parametrize("mutation,code", [
("metadata-width", "wrong-result-attribute-type"), ("attribute-ascii", "wrong-result-attribute-type"),
("missing-dataset", "wrong-result-quantity-inventory"), ("extra-component-dataset", "wrong-result-quantity-inventory"),
("compound-member-order", "wrong-result-table-schema"),
])
def test_required_types_and_complete_dataset_inventory(tmp_path, result_fixtures, mutation, code):
path = copy_result(tmp_path, result_fixtures)
with h5py.File(path, "r+") as file:
if mutation == "metadata-width":
obj = file["metadata"]
del obj.attrs["schema_version"]
obj.attrs.create("schema_version", 1, dtype="<u4")
elif mutation == "attribute-ascii":
obj = file["steps/Step-1/frames/0/nodal/displacement"]
del obj.attrs["unit_dimensions"]
obj.attrs.create("unit_dimensions", b"length", dtype="S6")
elif mutation == "missing-dataset":
del file["steps/Step-1/frames/0/nodal/reaction"]
elif mutation == "extra-component-dataset":
file.create_dataset("steps/Step-1/frames/0/nodal/rotation", data=np.zeros((442, 3)))
else:
obj = file["model/solid_integration_points"]
rows = obj[:]
del file[obj.name]
compound(file["model"], "solid_integration_points", [
("point_id", "u1"), ("internal_element_id", "<u8"),
("natural_coordinates", "<f8", (3,)), ("reference_coordinates", "<f8", (3,))],
[(row["point_id"], row["internal_element_id"], row["natural_coordinates"], row["reference_coordinates"]) for row in rows])
assert compare_fixture(path)["precheck"]["code"] == code
def test_reference_raw_hash_rejects_modified_original_copy(tmp_path, result_fixtures):
for quantity in ("U", "RF", "S"):
shutil.copyfile(REFERENCE / f"hexa_linearstatic_{quantity}.csv", tmp_path / f"hexa_linearstatic_{quantity}.csv")
path = tmp_path / "hexa_linearstatic_U.csv"
path.write_bytes(path.read_bytes() + b"\r\n")
report = COMPARE.compare("hexa_linearstatic", REFERENCE / "hexa_linearstatic.inp", result_fixtures["hexa_linearstatic"], tmp_path)
assert report["precheck"]["code"] == "reference-hash-mismatch"
def test_case_families_cannot_share_scale_or_cancel_sign_errors():
assert family([100.], [100.])["passed"]
assert not family([.001], [0.])["passed"]
report = family([21., 19.], [20., 20.])
assert report["rows_passed"]
assert report["absolute_rms"] == 1.
assert report["relative_rms"] == .05
assert not report["rms_passed"]
def test_alternate_reference_directory_cannot_make_original_report_writable(tmp_path, monkeypatch):
original = REFERENCE / "hexa_linearstatic_U.csv"
original_sha = COMPARE.sha256(original)
monkeypatch.setattr(sys, "argv", ["compare_hex8.py", "--case", "hexa_linearstatic",
"--input", str(REFERENCE / "hexa_linearstatic.inp"), "--results", str(tmp_path / "fixture.h5"),
"--references", str(tmp_path / "copied-references"), "--report", str(original)])
monkeypatch.setattr(COMPARE, "compare", lambda *args: {"precheck": {"passed": True},
"case": "hexa_linearstatic", "verdict": "passed", "blocking_failures": [], "blocking_passed": True})
# This tripwire proves the old branch without ever allowing an original write.
write_text = Path.write_text
def protected_write(path, *args, **kwargs):
if path.resolve() == original.resolve():
pytest.fail("Comparator attempted to write an authoritative original reference")
return write_text(path, *args, **kwargs)
monkeypatch.setattr(Path, "write_text", protected_write)
with pytest.raises(SystemExit) as error:
COMPARE.main()
assert error.value.code == 2
assert COMPARE.sha256(original) == original_sha
@pytest.mark.parametrize("shape", [(), (10, 1)])
def test_modal_pair_rank_errors_produce_failed_precheck(tmp_path, result_fixtures, shape):
path = copy_result(tmp_path, result_fixtures, "hexa_freq")
with h5py.File(path, "r+") as file:
key = "steps/Step-1/eigen/verification/pairs"
del file[key]
file.create_dataset(key, shape=shape, dtype="<f8")
report = compare_fixture(path, "hexa_freq")
assert not report["precheck"]["passed"]
assert report["precheck"]["code"] == "wrong-modal-pair-shape"
assert report["families"] == []
def test_hardlink_report_alias_cannot_overwrite_known_source(tmp_path, monkeypatch):
protected = tmp_path / "originals"
protected.mkdir()
original = protected / "reference.csv"
original.write_bytes(b"immutable source fixture\n")
report_alias = tmp_path / "report-alias.json"
os.link(original, report_alias)
original_sha = COMPARE.sha256(original)
monkeypatch.setattr(COMPARE, "REFERENCE_DIRECTORY", protected)
monkeypatch.setattr(sys, "argv", ["compare_hex8.py", "--case", "hexa_linearstatic",
"--input", str(tmp_path / "input.inp"), "--results", str(tmp_path / "fixture.h5"),
"--references", str(tmp_path / "other-references"), "--report", str(report_alias)])
monkeypatch.setattr(COMPARE, "compare", lambda *args: {"precheck": {"passed": True},
"case": "hexa_linearstatic", "verdict": "passed", "blocking_failures": [], "blocking_passed": True})
# Neither the fixture nor its alias is written during the failing old path.
monkeypatch.setattr(Path, "write_text", lambda *args, **kwargs: pytest.fail("Unsafe alias write attempted"))
with pytest.raises(SystemExit) as error:
COMPARE.main()
assert error.value.code == 2
assert COMPARE.sha256(original) == original_sha
def test_lexically_equivalent_input_path_preserves_writer_identity(result_fixtures):
source = REFERENCE / "../hexa/hexa_linearstatic.inp"
report = COMPARE.compare("hexa_linearstatic", source, result_fixtures["hexa_linearstatic"])
assert report["verdict"] == "passed", report["precheck"]