"""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="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", "