Integrate Refactor solver sources and dual MSVC CMake builds

This commit is contained in:
KOKO\Mimi
2026-10-18 02:10:51 +09:00
parent 7ee32e5837
commit 12d51480de
218 changed files with 17842 additions and 9689 deletions
+1
View File
@@ -39,6 +39,7 @@ add_executable(
unit/core/diagnostic_test.cpp
unit/core/source_identity_test.cpp
unit/core/status_test.cpp
unit/elements/element_capabilities_test.cpp
unit/elements/element_factory_test.cpp
unit/elements/hex8_element_test.cpp
unit/elements/hex8_high_low_test.cpp
@@ -223,16 +223,15 @@ std::vector<std::uint8_t> ReadConstraintMask(const Hdf5File& file) {
fesa::Vector3 SpatialMoment(const fesa::Vector3& phi,
const fesa::Vector3& generalized) {
const fesa::Matrix transform = fesa::InverseTransposeLeftJacobianSO3(phi);
return {transform(0U, 0U) * generalized.X() +
transform(0U, 1U) * generalized.Y() +
transform(0U, 2U) * generalized.Z(),
transform(1U, 0U) * generalized.X() +
transform(1U, 1U) * generalized.Y() +
transform(1U, 2U) * generalized.Z(),
transform(2U, 0U) * generalized.X() +
transform(2U, 1U) * generalized.Y() +
transform(2U, 2U) * generalized.Z()};
// Independent closed-form action, used only as a test oracle.
const double angle = phi.Norm();
const double square = angle * angle;
const double coefficient = angle < 1e-4 ?
1.0 / 12.0 + square / 720.0 + square * square / 30240.0 +
square * square * square / 1209600.0 :
(1.0 - 0.5 * angle / std::tan(0.5 * angle)) / square;
return generalized + 0.5 * phi.Cross(generalized) +
coefficient * phi.Cross(phi.Cross(generalized));
}
std::array<double, 6> ReconstructGlobalBalance(const Hdf5File& file) {
@@ -19,6 +19,8 @@
#include "thermal_gauss_oracle.h"
#include "thermal_slab_oracle.h"
#include "fesa/assembly/thermal_assembler.h"
namespace {
namespace ti = fesa::thermal_transient_internal;
using fesa::Vector;
@@ -29,6 +29,8 @@
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "fesa/analysis/transient_heat_procedure.h"
namespace {
class HdfHandle {
+10 -1
View File
@@ -32,7 +32,16 @@ std::string ReadBytes(const std::filesystem::path& path) {
TEST(EigenReference, RunsOriginalInputAndComparesAllModalFrameIdentities) {
const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
const std::filesystem::path reference_directory =
source_root / "reference" / "shellNL-eigen-meshfine";
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
"eigen-execution";
std::filesystem::create_directories(reference_directory);
for (const auto& name : {"shellNL-eigen-meshfine.inp",
"shellNL-eigen-meshfine eigenvalues.txt"}) {
std::filesystem::copy_file(
source_root / "reference" / "shellNL-eigen-meshfine" / name,
reference_directory / name,
std::filesystem::copy_options::overwrite_existing);
}
const std::filesystem::path input =
reference_directory / "shellNL-eigen-meshfine.inp";
const std::filesystem::path text =
@@ -1193,16 +1193,15 @@ fesa::Vector3 VectorAt(const std::vector<double>& values,
fesa::Vector3 SpatialMoment(const fesa::Vector3& phi,
const fesa::Vector3& generalized) {
const Matrix transform = InverseTransposeLeftJacobianSO3(phi);
return {transform(0U, 0U) * generalized.X() +
transform(0U, 1U) * generalized.Y() +
transform(0U, 2U) * generalized.Z(),
transform(1U, 0U) * generalized.X() +
transform(1U, 1U) * generalized.Y() +
transform(1U, 2U) * generalized.Z(),
transform(2U, 0U) * generalized.X() +
transform(2U, 1U) * generalized.Y() +
transform(2U, 2U) * generalized.Z()};
// Independent closed-form action, used only as a test oracle.
const double angle = phi.Norm();
const double square = angle * angle;
const double coefficient = angle < 1e-4 ?
1.0 / 12.0 + square / 720.0 + square * square / 30240.0 +
square * square * square / 1209600.0 :
(1.0 - 0.5 * angle / std::tan(0.5 * angle)) / square;
return generalized + 0.5 * phi.Cross(generalized) +
coefficient * phi.Cross(phi.Cross(generalized));
}
fesa::Vector3 MaskedResidual(const Hdf5Projection& projection,
@@ -15,7 +15,14 @@ std::string Bytes(const std::filesystem::path& path) {
}
TEST(ThermalReference, OriginalAbaqusNt11RemainsAnExplicitFailingDiagnostic) {
const auto folder =
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "reference/shell-thermal";
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference/thermal-execution";
std::filesystem::create_directories(folder);
for (const auto& name : {"shell-thermal.inp",
"shell-thermal NodalTemperature.csv"}) {
std::filesystem::copy_file(
std::filesystem::path{FESA_TEST_SOURCE_DIR} / "reference/shell-thermal" / name,
folder / name, std::filesystem::copy_options::overwrite_existing);
}
const auto input = folder / "shell-thermal.inp";
const auto csv = folder / "shell-thermal NodalTemperature.csv";
const auto before_input = Bytes(input), before_csv = Bytes(csv);
+856
View File
@@ -0,0 +1,856 @@
"""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"]
+351
View File
@@ -0,0 +1,351 @@
"""HHT reference identity, time, projection and common-tolerance regressions."""
import csv
import json
import math
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
h5py = pytest.importorskip("h5py", reason="Run with uv --with h5py for HHT comparison tests.")
np = pytest.importorskip("numpy")
from scripts.verification import compare_hht_dynamic as comparator
@pytest.fixture(scope="module")
def artifacts(tmp_path_factory):
directory = tmp_path_factory.mktemp("hht-comparison")
input_path = comparator.REFERENCE_DIRECTORY / f"{comparator.CASE}.inp"
reference_path = comparator.REFERENCE_DIRECTORY / f"{comparator.CASE} displacements.csv"
nodes, content_identity = comparator.input_inventory(input_path)
reference = comparator.read_reference(reference_path)
result_path = directory / "matching_results.h5"
string_type = h5py.string_dtype("utf-8")
node_type = np.dtype([("internal_node_id", "<u8"), ("instance_name", string_type),
("source_label", string_type), ("coordinates", "<f8", (3,))])
node_rows = np.empty(729, dtype=node_type)
labels = list(nodes)
for index, label in enumerate(labels):
node_rows[index] = (index, "PART-1-1", str(label), nodes[label])
frame_type = np.dtype([("frame_index", "<u8"), ("increment_index", "<u8"),
("time", "<f8"), ("iteration_count", "<u8"), ("is_initial", "u1")])
frames = np.empty(101, dtype=frame_type)
with h5py.File(result_path, "w") as file:
metadata = file.create_group("metadata")
metadata.attrs.update({
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"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-1", "step_name": "Step-1", "unit_system_label": "consistent-input-units",
"solver_version": "test-fixture", "source_input_identity": f"path={input_path};content_identity={content_identity}",
"schema_version": np.uint64(1), "increment_count": np.uint64(100), "maximum_increments": np.uint64(1000),
"alpha": -0.05, "beta": 0.275625, "gamma": 0.55, "initial_increment": 0.1, "time_period": 10.0,
})
file.create_dataset("model/nodes", data=node_rows)
for increment in range(101):
time = float(increment) * 0.1
frames[increment] = (increment, increment, time, 0 if increment == 0 else 2, int(increment == 0))
values = np.zeros((729, 6), dtype="<f8")
for label in (2, 21, 22, 69, 70, 165, 309, 442, 586):
values[labels.index(label)] = reference[(increment, label)]
dataset = file.create_dataset(f"steps/Step-1/frames/{increment}/nodal/displacement", data=values)
dataset.attrs.update({
"component_names": "UX,UY,UZ,URX,URY,URZ", "component_unit_dimensions": "length,length,length,1,1,1",
"coordinate_system": "global-cartesian", "location": "nodal", "step_name": "Step-1", "source_step_name": "Step-1",
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "global-translation-and-total-rotation-vector",
"schema_version": np.uint64(1), "frame_index": np.uint64(increment), "increment_index": np.uint64(increment),
"time": time, "is_initial": np.uint8(increment == 0),
})
file.create_dataset("steps/Step-1/accepted_frames", data=frames)
return input_path, reference_path, result_path
def changed_result(tmp_path, artifacts):
target = tmp_path / "changed.h5"
shutil.copyfile(artifacts[2], target)
return target
def reduced_result(tmp_path, artifacts):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
file["metadata"].attrs.modify("schema_version", np.uint64(2))
file["metadata"].attrs["time_integration_formulation"] = (
"reduced-coordinate-HHT-with-physical-rate-reconstruction")
for frame in range(101):
root = f"steps/Step-1/frames/{frame}/nodal"
displacement = file[f"{root}/displacement"]
displacement.attrs.modify("schema_version", np.uint64(2))
auxiliary = file.create_dataset(f"{root}/algorithmic_acceleration",
data=np.zeros((729, 6), dtype="<f8"))
for name, value in displacement.attrs.items():
auxiliary.attrs[name] = value
auxiliary.attrs["component_names"] = "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6"
auxiliary.attrs["coordinate_system"] = "fixed-initial-global-chart"
auxiliary.attrs["component_unit_dimensions"] = (
"length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2")
auxiliary.attrs["quantity_semantics"] = (
"full-chart-HHT-filter-combination-not-predictor-driver")
return path
def test_reduced_schema_preserves_displacement_comparison(tmp_path, artifacts):
result = comparator.compare(artifacts[0], artifacts[1], reduced_result(tmp_path, artifacts))
assert result["precheck"]["passed"], result["precheck"]
assert result["verdict"] == "passed"
@pytest.mark.parametrize("mutation", ["missing_A", "A_semantics", "A_version", "A_nan", "method"])
def test_reduced_schema_rejects_inconsistent_auxiliary_history(tmp_path, artifacts, mutation):
path = reduced_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
auxiliary = file["steps/Step-1/frames/99/nodal/algorithmic_acceleration"]
if mutation == "missing_A":
del file[auxiliary.name]
elif mutation == "A_semantics":
auxiliary.attrs["quantity_semantics"] = "auxiliary-HHT-chart-acceleration"
elif mutation == "A_version":
auxiliary.attrs.modify("schema_version", np.uint64(1))
elif mutation == "A_nan":
auxiliary[0, 0] = float("nan")
else:
file["metadata"].attrs["time_integration_formulation"] = "full-coordinate-HHT"
result = comparator.compare(artifacts[0], artifacts[1], path)
assert not result["precheck"]["passed"]
assert result["families"] == []
def test_original_csv_exact_matching_history_and_deterministic_report(artifacts):
first = comparator.compare(*artifacts)
second = comparator.compare(*artifacts)
assert first == second
assert first["precheck"]["passed"], first["precheck"]
assert first["precheck"]["reference_row_count"] == 909
assert first["precheck"]["checked_result_scalars"] == 441774
assert first["verdict"] == "passed"
assert first["warnings"] == []
assert len(first["families"]) == 202
assert sum(family["row_count"] for family in first["families"]) == 5454
assert [family["quantity"] for family in first["families"][:4]] == ["U", "UR", "U", "UR"]
assert all(family["passed"] for family in first["families"])
assert first["families"][0]["reference_scale"] == 0.0
assert first["families"][6]["hdf5_time"] == 3 * 0.1
assert first["families"][6]["reference_decimal_time"] == "0.3"
assert 3 * 0.1 != float("0.3") # Both legitimate representations were validated independently.
json.dumps(first, allow_nan=False)
@pytest.mark.parametrize("mutation,code", [
("procedure", "wrong-hdf5-attribute-value"),
("mass", "wrong-hdf5-attribute-value"),
("coefficient", "wrong-hdf5-attribute-value"),
("initial_convention", "wrong-hdf5-attribute-value"),
("input_content", "wrong-input-content-identity"),
("component", "wrong-hdf5-attribute-value"),
("rotation", "wrong-hdf5-attribute-value"),
("missing_component_attribute", "missing-hdf5-attribute"),
("nonprojected_nan", "nonfinite-result-value"),
("rotation_inf", "nonfinite-result-value"),
("initial_motion", "nonzero-initial-displacement"),
("duplicate_node", "duplicate-result-node"),
("extra_node_label", "extra-result-node"),
("wrong_instance", "wrong-result-instance"),
("coordinate", "wrong-result-coordinate"),
("wrong_shape", "wrong-hdf5-dataset-layout"),
("float32", "wrong-displacement-type"),
("missing_frame", "result-frame-inventory"),
("extra_frame", "result-frame-inventory"),
("leading_zero_frame", "result-frame-inventory"),
("duplicate_increment", "wrong-result-frame-time"),
("decimal_binary_time", "wrong-result-frame-time"),
("frame_attribute_time", "wrong-hdf5-attribute-value"),
("trial_iteration", "wrong-result-frame-time"),
("initial_iteration", "wrong-result-frame-time"),
])
def test_hdf5_precheck_rejects_before_tolerance(tmp_path, artifacts, mutation, code):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
metadata = file["metadata"]
displacement = file["steps/Step-1/frames/3/nodal/displacement"]
nodes = file["model/nodes"]
table = file["steps/Step-1/accepted_frames"]
if mutation in {"procedure", "mass", "coefficient", "initial_convention", "input_content"}:
key, value = {
"procedure": ("analysis_procedure", "linear-static"),
"mass": ("mass_formulation", "consistent-kinetic"),
"coefficient": ("alpha", 0.0),
"initial_convention": ("initial_state", "unloaded"),
"input_content": ("source_input_identity", "wrong-content"),
}[mutation]
metadata.attrs.modify(key, value)
elif mutation in {"component", "rotation", "frame_attribute_time"}:
key, value = {"component": ("component_names", "U1,U2,U3,UR1,UR2,UR3"),
"rotation": ("rotation_representation", "incremental-rotation"),
"frame_attribute_time": ("time", 0.3)}[mutation]
displacement.attrs.modify(key, value)
elif mutation == "missing_component_attribute":
del displacement.attrs["component_names"]
elif mutation in {"nonprojected_nan", "rotation_inf"}:
displacement[0, 0 if mutation == "nonprojected_nan" else 3] = float("nan") if mutation == "nonprojected_nan" else float("inf")
elif mutation == "initial_motion":
file["steps/Step-1/frames/0/nodal/displacement"][0, 0] = 1.0e-100
elif mutation in {"duplicate_node", "extra_node_label", "wrong_instance", "coordinate"}:
rows = nodes[:]
if mutation == "duplicate_node":
rows[1]["source_label"] = rows[0]["source_label"]
elif mutation == "extra_node_label":
rows[0]["source_label"] = "99999"
elif mutation == "wrong_instance":
rows[0]["instance_name"] = "Other"
else:
rows[0]["coordinates"] = [1.0, 2.0, 3.0]
nodes[:] = rows
elif mutation in {"wrong_shape", "float32"}:
name = displacement.name
values = displacement[:]
del file[name]
file.create_dataset(name, data=values[:-1] if mutation == "wrong_shape" else values.astype("<f4"))
elif mutation == "missing_frame":
del file["steps/Step-1/frames/100"]
elif mutation == "extra_frame":
file.create_group("steps/Step-1/frames/101")
elif mutation == "leading_zero_frame":
file.move("steps/Step-1/frames/1", "steps/Step-1/frames/01")
else:
rows = table[:]
if mutation == "duplicate_increment":
rows[3]["increment_index"] = 2
elif mutation == "decimal_binary_time":
rows[3]["time"] = 0.3
elif mutation == "trial_iteration":
rows[3]["iteration_count"] = 101
elif mutation == "initial_iteration":
rows[0]["iteration_count"] = 1
table[:] = rows
report = comparator.compare(artifacts[0], artifacts[1], path)
assert report["precheck"]["passed"] is False
assert report["precheck"]["code"] == code, report["precheck"]
assert report["families"] == []
assert report["verdict"] == "failed"
@pytest.mark.parametrize("mutation,code", [
("duplicate_header", "duplicate-reference-column"),
("missing_header", "missing-reference-column"),
("duplicate_row", "duplicate-reference-row"),
("missing_row", "reference-row-inventory"),
("extra_node", "reference-row-inventory"),
("extra_frame", "reference-row-inventory"),
("time", "wrong-reference-time"),
("instance", "wrong-reference-instance"),
("label", "invalid-node-label"),
("nonfinite_rotation", "nonfinite-reference-value"),
])
def test_reference_precheck_rejects_before_tolerance(tmp_path, artifacts, mutation, code):
with artifacts[1].open(newline="", encoding="utf-8") as stream:
rows = list(csv.reader(stream))
if mutation == "duplicate_header":
rows[0][-1] = rows[0][-2]
elif mutation == "missing_header":
rows[0][-1] = "Not a required component"
elif mutation == "duplicate_row":
rows.append(rows[1])
elif mutation == "missing_row":
rows.pop()
elif mutation in {"extra_node", "extra_frame"}:
row = rows[1].copy()
if mutation == "extra_node":
row[2] = "1"
else:
row[0] = "Increment 101: Step Time = 10.1"
rows.append(row)
elif mutation == "time":
rows[1][0] = "Increment 0: Step Time = 0.0000000001"
elif mutation == "instance":
rows[1][1] = "WRONG-INSTANCE"
elif mutation == "label":
rows[1][2] = "2.0"
else:
rows[1][-1] = "nan"
reference = tmp_path / "changed.csv"
with reference.open("w", newline="", encoding="utf-8") as stream:
csv.writer(stream).writerows(rows)
report = comparator.compare(artifacts[0], reference, artifacts[2])
assert not report["precheck"]["passed"]
assert report["precheck"]["code"] == code, report["precheck"]
assert report["families"] == []
def test_projection_joins_source_identity_and_csv_order_is_irrelevant(tmp_path, artifacts):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
nodes = file["model/nodes"][:][::-1]
nodes["internal_node_id"] = np.arange(729, dtype="<u8")
file["model/nodes"][:] = nodes
for frame in range(101):
values = file[f"steps/Step-1/frames/{frame}/nodal/displacement"]
values[:] = values[:][::-1]
with artifacts[1].open(newline="", encoding="utf-8") as stream:
rows = list(csv.reader(stream))
reference = tmp_path / "reordered.csv"
with reference.open("w", newline="", encoding="utf-8") as stream:
csv.writer(stream).writerows([rows[0], *reversed(rows[1:])])
report = comparator.compare(artifacts[0], reference, path)
assert report["precheck"]["passed"], report["precheck"]
assert report["verdict"] == "passed"
@pytest.mark.parametrize("component,passed", [(0, False), (3, True)])
def test_translation_blocks_rotation_warns(tmp_path, artifacts, component, passed):
path = changed_result(tmp_path, artifacts)
with h5py.File(path, "r+") as file:
values = file["steps/Step-1/frames/1/nodal/displacement"]
rows = values[:]
rows[:, component:component + 3] *= 2.0
values[:] = rows
report = comparator.compare(artifacts[0], artifacts[1], path)
assert report["precheck"]["passed"]
assert report["blocking_passed"] is passed
assert len(report["warnings"]) == (2 if passed else 0)
if passed:
assert [warning["code"] for warning in report["warnings"]] == ["row-tolerance-failure", "relative-rms-failure"]
else:
assert report["blocking_failures"] == ["U-global:Step-1:increment-1"]
def test_common_policy_boundaries_and_zero_scale():
near = comparator.evaluate_family(1, "U", [100.0, 2.0, 1.0], [100.0, 1.0, 0.0])
assert near["passed"]
assert [row["branch"] for row in near["rows"]] == ["relative", "near-zero", "near-zero"]
relative = comparator.evaluate_family(1, "U", [105.0], [100.0])
assert relative["rows_passed"] and not relative["rms_passed"]
boundary = comparator.evaluate_family(1, "U", [101.0], [100.0])
assert boundary["passed"]
assert boundary["relative_rms"] == 0.01
outside = comparator.evaluate_family(1, "U", [math.nextafter(101.0, math.inf)], [100.0])
assert outside["rows_passed"] and not outside["rms_passed"]
zero = comparator.evaluate_family(0, "U", [0.0], [0.0])
assert zero["passed"] and zero["relative_rms"] is None
nonzero = comparator.evaluate_family(0, "U", [1.0e-300], [0.0])
assert not nonzero["passed"]
assert nonzero["diagnostic_code"] == "zero-reference-scale-nonzero-error"
json.dumps(nonzero, allow_nan=False)
with pytest.raises(comparator.ComparisonError, match="U"):
comparator.evaluate_family(1, "U", [1.0e308], [-1.0e308])
def test_cli_writes_json_and_exit_two_for_missing_results(tmp_path, artifacts):
report = tmp_path / "evidence.json"
completed = subprocess.run([sys.executable, "-B", str(Path(comparator.__file__)),
"--input", str(artifacts[0]), "--reference", str(artifacts[1]),
"--results", str(tmp_path / "absent.h5"), "--report", str(report)],
check=False, capture_output=True, text=True)
assert completed.returncode == 2, completed.stderr
evidence = json.loads(report.read_text())
assert not evidence["precheck"]["passed"]
assert evidence["families"] == []
@@ -7,6 +7,9 @@
#include <type_traits>
#include <utility>
#include "fesa/analysis/analysis_model.h"
#include "fesa/fem/dof_manager.h"
namespace {
template <class T, class = void>
+563
View File
@@ -1,9 +1,20 @@
#include "fesa/analysis/analysis.h"
#include "fesa/analysis/analysis_procedure.h"
#include "fesa/analysis/prepared_analysis.h"
#include "analysis/equilibrium_algorithm.h"
#include "analysis/hht_reduced_integrator.h"
#include "analysis/hht_reduced_procedure.h"
#include "analysis/solid_hht_integrator.h"
#include "analysis/solid_hht_core.h"
#include "analysis/thermal_transient_algebra.h"
#include "fesa/solvers/linear/tangent_solver.h"
#include <gtest/gtest.h>
#include <filesystem>
#include <memory>
#include <optional>
#include <vector>
#include <type_traits>
#include <utility>
@@ -65,3 +76,555 @@ TEST(Analysis, PropagatesStatusAndDestroysThroughMinimalAnalysis) {
analysis.reset();
EXPECT_TRUE(destroyed);
}
namespace {
using ProcedureSignature = fesa::Result<fesa::PreparedAnalysisOutcome> (
fesa::AnalysisProcedure::*)(const fesa::Domain &);
static_assert(std::has_virtual_destructor_v<fesa::AnalysisProcedure>);
static_assert(std::is_abstract_v<fesa::AnalysisProcedure>);
static_assert(std::is_same_v<decltype(&fesa::AnalysisProcedure::Execute),
ProcedureSignature>);
class ObservedProcedure final : public fesa::AnalysisProcedure {
public:
ObservedProcedure(const fesa::Domain *&observed, bool &destroyed)
: observed_(observed), destroyed_(destroyed) {}
~ObservedProcedure() override { destroyed_ = true; }
fesa::Result<fesa::PreparedAnalysisOutcome>
Execute(const fesa::Domain &domain) override {
observed_ = &domain;
return fesa::Result<fesa::PreparedAnalysisOutcome>::Failure(
fesa::Status::Failure(fesa::FailureCategory::kSolver,
{{fesa::Severity::kError,
"procedure-original-failure",
{},
"ORIGINAL",
"source-identity",
"Original failure message."}}));
}
private:
const fesa::Domain *&observed_;
bool &destroyed_;
};
} // namespace
TEST(AnalysisProcedure, PreparedConsumerPreservesDomainFailureAndLifetime) {
const auto domain_result = fesa::Domain::Create({});
ASSERT_TRUE(domain_result.HasValue());
const auto &domain = domain_result.Value();
const fesa::Domain *observed = nullptr;
bool destroyed = false;
std::unique_ptr<fesa::AnalysisProcedure> procedure =
std::make_unique<ObservedProcedure>(observed, destroyed);
auto outcome = fesa::ExecutePreparedAnalysis(domain, *procedure);
EXPECT_EQ(observed, &domain);
ASSERT_FALSE(outcome.HasValue());
EXPECT_EQ(outcome.GetStatus().Category(), fesa::FailureCategory::kSolver);
ASSERT_EQ(outcome.GetStatus().Diagnostics().size(), 1U);
const auto &diagnostic = outcome.GetStatus().Diagnostics()[0];
EXPECT_EQ(diagnostic.code, "procedure-original-failure");
EXPECT_EQ(diagnostic.keyword, "ORIGINAL");
EXPECT_EQ(diagnostic.entity_identity, "source-identity");
EXPECT_EQ(diagnostic.message, "Original failure message.");
EXPECT_FALSE(destroyed);
procedure.reset();
EXPECT_TRUE(destroyed);
}
TEST(EquilibriumAlgorithm, RetainsMoveOnlyCandidateUntilProcedureTakesIt) {
static_assert(std::has_virtual_destructor_v<fesa::EquilibriumAlgorithm>);
static_assert(std::is_abstract_v<fesa::EquilibriumAlgorithm>);
using Candidate = std::unique_ptr<int>;
using EvaluationResult = fesa::Result<std::optional<Candidate>>;
std::vector<std::uint32_t> observed;
fesa::EquilibriumStep<Candidate, 1U> step(
[&](std::uint32_t iteration) {
observed.push_back(iteration);
if (iteration == 3U)
return EvaluationResult::Success(std::make_unique<int>(42));
return EvaluationResult::Success(std::nullopt);
}, 100U);
fesa::NewtonStepAlgorithm algorithm;
fesa::EquilibriumAlgorithm& base = algorithm;
EXPECT_FALSE(step.HasCandidate());
ASSERT_TRUE(step.Solve(base).IsOk());
EXPECT_EQ(observed, (std::vector<std::uint32_t>{1U, 2U, 3U}));
ASSERT_TRUE(step.HasCandidate());
Candidate accepted = step.TakeCandidate();
ASSERT_NE(accepted, nullptr);
EXPECT_EQ(*accepted, 42);
}
TEST(EquilibriumAlgorithm, FirstFailedEvaluationLeavesNoAcceptedCandidate) {
using EvaluationResult = fesa::Result<std::optional<int>>;
std::vector<std::uint32_t> observed;
fesa::EquilibriumStep<int, 1U> step(
[&](std::uint32_t iteration) {
observed.push_back(iteration);
return EvaluationResult::Failure(fesa::Status::Failure(
fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "first-evaluation-failure", {},
"STATIC", "original-owner", "Original evaluation failure."}}));
}, 100U);
fesa::NewtonStepAlgorithm algorithm;
fesa::EquilibriumAlgorithm& base = algorithm;
const auto status = step.Solve(base);
EXPECT_EQ(observed, (std::vector<std::uint32_t>{1U}));
EXPECT_FALSE(step.HasCandidate());
ASSERT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0].code, "first-evaluation-failure");
EXPECT_EQ(status.Diagnostics()[0].entity_identity, "original-owner");
EXPECT_EQ(status.Diagnostics()[0].message, "Original evaluation failure.");
}
namespace {
// These helpers route supplied values only. They never call a concrete time
// integrator, Domain/assembly/time-grid builder, analysis or numerical solver.
fesa::Status InjectionFailure() {
return fesa::Status::Failure(fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "injected-policy-failure", {}, "MOCK",
"caller-owned", "Original injected failure."}});
}
void ExpectInjectionFailure(const fesa::Status& status) {
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver);
ASSERT_EQ(status.Diagnostics().size(), 1U);
const auto& diagnostic = status.Diagnostics()[0];
EXPECT_EQ(diagnostic.code, "injected-policy-failure");
EXPECT_EQ(diagnostic.keyword, "MOCK");
EXPECT_EQ(diagnostic.entity_identity, "caller-owned");
EXPECT_EQ(diagnostic.message, "Original injected failure.");
}
void ExpectValueVector(const fesa::Vector& value, double expected) {
ASSERT_EQ(value.Size(), 2U);
for (std::size_t i = 0; i < value.Size(); ++i)
EXPECT_DOUBLE_EQ(value[i], expected);
}
class NeverCalledLinear final : public fesa::LinearSolver {
public:
fesa::Status Factorize(const fesa::SparseMatrix&) override {
ADD_FAILURE() << "Mock dispatch must not factorize.";
return InjectionFailure();
}
fesa::Status Solve(const fesa::Vector&, fesa::Vector&) const override {
ADD_FAILURE() << "Mock dispatch must not solve.";
return InjectionFailure();
}
};
class NeverCalledTangent final : public fesa::TangentSolver {
public:
fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override {
ADD_FAILURE() << "Mock dispatch must not factorize.";
return InjectionFailure();
}
fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override {
ADD_FAILURE() << "Mock dispatch must not solve.";
return InjectionFailure();
}
};
class SelectedOrderAlgorithm final : public fesa::EquilibriumAlgorithm {
public:
explicit SelectedOrderAlgorithm(bool& destroyed) : destroyed_(destroyed) {}
~SelectedOrderAlgorithm() override { destroyed_ = true; }
bool fail = false;
fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation& evaluate) override {
EXPECT_EQ(first, 0U);
EXPECT_EQ(last, 3U);
if (fail) return InjectionFailure();
auto first_result = evaluate(first);
if (!first_result.HasValue()) return first_result.GetStatus();
EXPECT_FALSE(first_result.Value());
auto last_result = evaluate(last);
if (!last_result.HasValue()) return last_result.GetStatus();
EXPECT_TRUE(last_result.Value());
return fesa::Status::Ok();
}
private:
bool& destroyed_;
};
namespace shell_value = fesa::hht_reduced_internal;
class ValueShellIntegrator final : public shell_value::ShellHhtIntegrator {
public:
ValueShellIntegrator(bool& destroyed, std::vector<std::string>& events)
: destroyed_(destroyed), events_(events) {}
~ValueShellIntegrator() override { destroyed_ = true; }
std::function<shell_value::ReducedShellPredictor(
const shell_value::ReducedHistory&, double, const fesa::HhtParameters&)> predict;
std::function<void(const shell_value::ReducedShellRateRequest&,
fesa::Vector&, fesa::Vector&)> rates;
std::function<fesa::Result<shell_value::ReconstructedRates>(
const shell_value::FixedChart&, const shell_value::EmbeddingRoot&,
const fesa::Vector&, const fesa::Vector&, fesa::TangentSolver&)> reconstruct;
shell_value::ReducedShellPredictor Predict(
const shell_value::ReducedHistory& history, double h,
const fesa::HhtParameters& parameters) const override {
events_.push_back("predict");
return predict(history, h, parameters);
}
void EvaluateRates(const shell_value::ReducedShellRateRequest& request,
fesa::Vector& velocity, fesa::Vector& acceleration) const override {
events_.push_back("rates");
rates(request, velocity, acceleration);
}
fesa::Result<shell_value::ReconstructedRates> ReconstructPhysicalRates(
const shell_value::FixedChart& chart, const shell_value::EmbeddingRoot& root,
const fesa::Vector& velocity, const fesa::Vector& acceleration,
fesa::TangentSolver& solver) const override {
events_.push_back("reconstruct");
return reconstruct(chart, root, velocity, acceleration, solver);
}
private:
bool& destroyed_;
std::vector<std::string>& events_;
};
class ValueSolidIntegrator final : public fesa::detail::SolidHhtIntegrator {
public:
ValueSolidIntegrator(bool& destroyed, std::vector<std::string>& events)
: destroyed_(destroyed), events_(events) {}
~ValueSolidIntegrator() override { destroyed_ = true; }
std::function<fesa::Result<fesa::detail::SolidHhtPairedPredictor>(
const fesa::SolidDynamicAcceptedFrameState&, double,
const fesa::HhtParameters&)> predict;
std::function<fesa::Result<fesa::detail::SolidHhtPairedRates>(
const fesa::Vector&, const fesa::Vector&,
const fesa::detail::SolidHhtPairedPredictor&, const fesa::Vector&,
const fesa::Vector&, double, const fesa::HhtParameters&)> rates;
fesa::Result<fesa::detail::SolidHhtPairedPredictor> Predict(
const fesa::SolidDynamicAcceptedFrameState& old, double h,
const fesa::HhtParameters& parameters) const override {
events_.push_back("predict");
return predict(old, h, parameters);
}
fesa::Result<fesa::detail::SolidHhtPairedRates> EvaluateRates(
const fesa::Vector& high, const fesa::Vector& low,
const fesa::detail::SolidHhtPairedPredictor& predictor,
const fesa::Vector& old_acceleration,
const fesa::Vector& old_acceleration_low, double h,
const fesa::HhtParameters& parameters) const override {
events_.push_back("rates");
return rates(high, low, predictor, old_acceleration, old_acceleration_low,
h, parameters);
}
private:
bool& destroyed_;
std::vector<std::string>& events_;
};
namespace heat_value = fesa::thermal_transient_internal;
class ValueHeatIntegrator final : public heat_value::HeatStepIntegrator {
public:
ValueHeatIntegrator(bool& destroyed, std::vector<std::string>& events)
: destroyed_(destroyed), events_(events) {}
~ValueHeatIntegrator() override { destroyed_ = true; }
std::function<fesa::Result<heat_value::ThermalStepCandidate>(
const heat_value::ThermalSystem&, const heat_value::ThermalPartition&,
const fesa::Vector&, double, fesa::LinearSolver&,
heat_value::ThermalFactorization&)> solve;
fesa::Result<heat_value::ThermalStepCandidate> SolveIncrement(
const heat_value::ThermalSystem& system,
const heat_value::ThermalPartition& partition,
const fesa::Vector& old, double delta, fesa::LinearSolver& solver,
heat_value::ThermalFactorization& cache) const override {
events_.push_back("solve");
return solve(system, partition, old, delta, solver, cache);
}
private:
bool& destroyed_;
std::vector<std::string>& events_;
};
} // namespace
TEST(EquilibriumAlgorithm, InjectsAlternativeOrderAndPreservesTypedPendingOnFailure) {
bool destroyed = false;
auto owned = std::make_unique<SelectedOrderAlgorithm>(destroyed);
auto* selected = owned.get();
std::unique_ptr<fesa::EquilibriumAlgorithm> policy = std::move(owned);
std::vector<std::uint32_t> observed;
using Candidate = std::unique_ptr<int>;
using EvaluationResult = fesa::Result<std::optional<Candidate>>;
fesa::EquilibriumStep<Candidate, 0U> step(
[&](std::uint32_t iteration) {
observed.push_back(iteration);
return iteration == 3U
? EvaluationResult::Success(std::make_unique<int>(42))
: EvaluationResult::Success(std::nullopt);
}, 3U);
ASSERT_TRUE(step.Solve(*policy).IsOk());
EXPECT_EQ(observed, (std::vector<std::uint32_t>{0U, 3U}));
selected->fail = true;
ExpectInjectionFailure(step.Solve(*policy));
EXPECT_EQ(observed, (std::vector<std::uint32_t>{0U, 3U}));
ASSERT_TRUE(step.HasCandidate());
EXPECT_EQ(*step.TakeCandidate(), 42);
EXPECT_FALSE(destroyed);
policy.reset();
EXPECT_TRUE(destroyed);
// The default algorithm still evaluates zero through the inclusive maximum.
std::vector<std::uint32_t> zero_observed;
fesa::NewtonStepAlgorithm zero_algorithm;
fesa::EquilibriumStep<int, 0U> zero_step([&](std::uint32_t iteration) {
zero_observed.push_back(iteration);
return iteration == 2U ? fesa::Result<std::optional<int>>::Success(7)
: fesa::Result<std::optional<int>>::Success(std::nullopt);
}, 2U);
ASSERT_TRUE(zero_step.Solve(zero_algorithm).IsOk());
EXPECT_EQ(zero_observed, (std::vector<std::uint32_t>{0U, 1U, 2U}));
ASSERT_TRUE(zero_step.HasCandidate());
EXPECT_EQ(zero_step.TakeCandidate(), 7);
}
TEST(ProcedureIntegratorInjection, ShellValuesUseProductionStepAndPreserveHistory) {
bool destroyed = false;
std::vector<std::string> events;
auto owned = std::make_unique<ValueShellIntegrator>(destroyed, events);
auto* fake = owned.get();
std::unique_ptr<shell_value::ShellHhtIntegrator> integrator = std::move(owned);
shell_value::ReducedHistory old{fesa::Vector(2, 1), fesa::Vector(2, 2),
fesa::Vector(2, 3), fesa::Vector(2, 4), fesa::Vector(2, 5)};
shell_value::FixedChart chart;
shell_value::EmbeddingRoot root;
fesa::Vector algorithmic(2, 6), retained_v(2, 7), retained_a(2, 8);
const fesa::HhtParameters parameters{11, 12, 13};
NeverCalledTangent solver;
bool fail_reconstruction = false, fail_commit = false;
fake->predict = [&](const auto& history, double h, const auto& p) {
EXPECT_EQ(&history, &old); EXPECT_EQ(&p, &parameters); EXPECT_DOUBLE_EQ(h, 17);
return shell_value::ReducedShellPredictor{fesa::Vector(2, 21), fesa::Vector(2, 22),
fesa::Vector(2, 23), fesa::Vector(2, 24), fesa::Vector(2, 25), fesa::Vector(2, 26)};
};
fake->rates = [&](const auto& request, auto& velocity, auto& acceleration) {
EXPECT_EQ(&request.history, &old); EXPECT_EQ(&request.chart, &chart);
EXPECT_EQ(&request.trial_algorithmic, &algorithmic);
EXPECT_EQ(&request.retained_velocity, &retained_v);
EXPECT_EQ(&request.retained_acceleration, &retained_a);
EXPECT_EQ(&request.parameters, &parameters); EXPECT_DOUBLE_EQ(request.h, 17);
EXPECT_FALSE(request.copy_translation_rates); ExpectValueVector(request.vpp, 22);
EXPECT_NE(velocity.Data(), acceleration.Data());
EXPECT_NE(velocity.Data(), request.vpp.Data());
EXPECT_NE(acceleration.Data(), request.trial_algorithmic.Data());
ExpectValueVector(velocity, 0); ExpectValueVector(acceleration, 0);
for (std::size_t i = 0; i < 2; ++i) { velocity[i] = 31; acceleration[i] = 32; }
};
fake->reconstruct = [&](const auto& c, const auto& r, const auto& v,
const auto& a, auto& backend) {
EXPECT_EQ(&c, &chart); EXPECT_EQ(&r, &root); EXPECT_EQ(&backend, &solver);
ExpectValueVector(v, 31); ExpectValueVector(a, 32);
if (fail_reconstruction)
return fesa::Result<shell_value::ReconstructedRates>::Failure(InjectionFailure());
return fesa::Result<shell_value::ReconstructedRates>::Success({
fesa::Vector(2, 41), fesa::Vector(2, 42), fesa::Vector(2, 43), fesa::Vector(2, 44)});
};
std::vector<shell_value::ReconstructedRates> history;
const auto evaluate = [&](const auto& selected, auto predictor) {
ExpectValueVector(predictor.pp, 21); ExpectValueVector(predictor.vpp, 22);
ExpectValueVector(predictor.coordinate_low, 23);
ExpectValueVector(predictor.increment_trace, 24);
ExpectValueVector(predictor.increment_trace_compensation, 25);
ExpectValueVector(predictor.increment_trace_absolute_sum, 26);
shell_value::ReducedShellTrialRates rates{fesa::Vector(2), fesa::Vector(2)};
auto* v_data = rates.velocity.Data(); auto* a_data = rates.acceleration.Data();
selected.EvaluateRates({chart, old, algorithmic, predictor.vpp, false,
retained_v, retained_a, 17, parameters}, rates.velocity, rates.acceleration);
EXPECT_EQ(v_data, rates.velocity.Data()); EXPECT_EQ(a_data, rates.acceleration.Data());
return selected.ReconstructPhysicalRates(chart, root, rates.velocity,
rates.acceleration, solver);
};
const auto commit = [&](shell_value::ReconstructedRates&& pending) {
events.push_back("commit");
ExpectValueVector(pending.velocity, 41); ExpectValueVector(pending.acceleration, 42);
ExpectValueVector(pending.z_velocity, 43); ExpectValueVector(pending.z_acceleration, 44);
if (fail_commit) return InjectionFailure();
history.push_back(std::move(pending));
return fesa::Status::Ok();
};
ASSERT_TRUE(shell_value::RunReducedHhtStep(*integrator, old, 17, parameters,
evaluate, commit).IsOk());
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates", "reconstruct", "commit"}));
ASSERT_EQ(history.size(), 1U);
events.clear(); fail_reconstruction = true;
ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17,
parameters, evaluate, commit));
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates", "reconstruct"}));
EXPECT_EQ(history.size(), 1U);
events.clear(); fail_reconstruction = false; fail_commit = true;
ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17,
parameters, evaluate, commit));
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates", "reconstruct", "commit"}));
EXPECT_EQ(history.size(), 1U);
ExpectValueVector(old.p, 1); ExpectValueVector(old.velocity, 2);
ExpectValueVector(old.acceleration, 3); ExpectValueVector(old.algorithmic_acceleration, 4);
ExpectValueVector(old.z, 5); ExpectValueVector(history[0].velocity, 41);
EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed);
}
TEST(ProcedureIntegratorInjection, SolidPairedValuesUseProductionStepAndPreserveHistory) {
bool destroyed = false;
std::vector<std::string> events;
auto owned = std::make_unique<ValueSolidIntegrator>(destroyed, events);
auto* fake = owned.get();
std::unique_ptr<fesa::detail::SolidHhtIntegrator> integrator = std::move(owned);
fesa::SolidDynamicAcceptedFrameState old;
old.displacement = fesa::Vector(2, 1); old.low.displacement = fesa::Vector(2, 2);
old.velocity = fesa::Vector(2, 3); old.low.velocity = fesa::Vector(2, 4);
old.acceleration = fesa::Vector(2, 5); old.low.acceleration = fesa::Vector(2, 6);
old.algorithmic_acceleration = fesa::Vector(2, 7);
old.low.algorithmic_acceleration = fesa::Vector(2, 8);
const fesa::HhtParameters parameters{11, 12, 13};
bool fail_predict = false, fail_rates = false, fail_commit = false;
fake->predict = [&](const auto& frame, double h, const auto& p) {
EXPECT_EQ(&frame, &old); EXPECT_EQ(&p, &parameters); EXPECT_DOUBLE_EQ(h, 17);
if (fail_predict)
return fesa::Result<fesa::detail::SolidHhtPairedPredictor>::Failure(InjectionFailure());
return fesa::Result<fesa::detail::SolidHhtPairedPredictor>::Success({
fesa::Vector(2, 21), fesa::Vector(2, 22), fesa::Vector(2, 23), fesa::Vector(2, 24)});
};
fake->rates = [&](const auto& high, const auto& low, const auto& predictor,
const auto& old_a, const auto& old_a_low, double h, const auto& p) {
ExpectValueVector(high, 1); ExpectValueVector(low, 2);
ExpectValueVector(predictor.displacement, 21); ExpectValueVector(predictor.displacement_low, 22);
ExpectValueVector(predictor.velocity, 23); ExpectValueVector(predictor.velocity_low, 24);
EXPECT_EQ(&old_a, &old.acceleration); EXPECT_EQ(&old_a_low, &old.low.acceleration);
EXPECT_EQ(&p, &parameters); EXPECT_DOUBLE_EQ(h, 17);
if (fail_rates)
return fesa::Result<fesa::detail::SolidHhtPairedRates>::Failure(InjectionFailure());
return fesa::Result<fesa::detail::SolidHhtPairedRates>::Success({
fesa::Vector(2, 31), fesa::Vector(2, 32), fesa::Vector(2, 33),
fesa::Vector(2, 34), fesa::Vector(2, 35), fesa::Vector(2, 36)});
};
std::vector<fesa::SolidDynamicAcceptedFrameState> history;
const auto failure = [&](const fesa::Status& status) {
events.push_back("predict-failure"); return status;
};
const auto evaluate = [&](const auto& selected, const auto& predictor) {
auto pending = old;
const auto status = fesa::detail::SetSolidHhtRates(selected, old, predictor,
17, parameters, &pending);
if (!status.IsOk()) {
ExpectValueVector(pending.velocity, 3); ExpectValueVector(pending.low.velocity, 4);
ExpectValueVector(pending.acceleration, 5); ExpectValueVector(pending.low.acceleration, 6);
ExpectValueVector(pending.algorithmic_acceleration, 7);
ExpectValueVector(pending.low.algorithmic_acceleration, 8);
return fesa::Result<fesa::SolidDynamicAcceptedFrameState>::Failure(status);
}
return fesa::Result<fesa::SolidDynamicAcceptedFrameState>::Success(std::move(pending));
};
const auto commit = [&](fesa::SolidDynamicAcceptedFrameState&& pending) {
events.push_back("commit");
ExpectValueVector(pending.displacement, 1); ExpectValueVector(pending.low.displacement, 2);
ExpectValueVector(pending.velocity, 31); ExpectValueVector(pending.low.velocity, 32);
ExpectValueVector(pending.acceleration, 33); ExpectValueVector(pending.low.acceleration, 34);
ExpectValueVector(pending.algorithmic_acceleration, 35);
ExpectValueVector(pending.low.algorithmic_acceleration, 36);
if (fail_commit) return InjectionFailure();
history.push_back(std::move(pending)); return fesa::Status::Ok();
};
ASSERT_TRUE(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters,
failure, evaluate, commit).IsOk());
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates", "commit"}));
ASSERT_EQ(history.size(), 1U);
events.clear(); fail_predict = true;
ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17,
parameters, failure, evaluate, commit));
EXPECT_EQ(events, (std::vector<std::string>{"predict", "predict-failure"}));
EXPECT_EQ(history.size(), 1U);
events.clear(); fail_predict = false; fail_rates = true;
ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17,
parameters, failure, evaluate, commit));
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates"}));
EXPECT_EQ(history.size(), 1U);
events.clear(); fail_rates = false; fail_commit = true;
ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17,
parameters, failure, evaluate, commit));
EXPECT_EQ(events, (std::vector<std::string>{"predict", "rates", "commit"}));
EXPECT_EQ(history.size(), 1U);
ExpectValueVector(old.displacement, 1); ExpectValueVector(old.low.displacement, 2);
ExpectValueVector(old.velocity, 3); ExpectValueVector(old.low.velocity, 4);
ExpectValueVector(old.acceleration, 5); ExpectValueVector(old.low.acceleration, 6);
ExpectValueVector(old.algorithmic_acceleration, 7); ExpectValueVector(old.low.algorithmic_acceleration, 8);
ExpectValueVector(history[0].low.algorithmic_acceleration, 36);
EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed);
}
TEST(ProcedureIntegratorInjection, HeatValuesUseProductionRecoveryCommitAndPreserveHistory) {
bool destroyed = false;
std::vector<std::string> events;
auto owned = std::make_unique<ValueHeatIntegrator>(destroyed, events);
auto* fake = owned.get();
std::unique_ptr<heat_value::HeatStepIntegrator> integrator = std::move(owned);
auto matrix = fesa::SparseMatrix::FromCoo(
2, 2, {}, fesa::SparsePattern{{0, 0, 0}, {}});
ASSERT_TRUE(matrix.HasValue());
const auto& k = matrix.Value();
const fesa::SparseMatrix c{k};
const fesa::Vector q(2, 2), old(2, 1);
const heat_value::ThermalSystem system{k, c, q};
const heat_value::ThermalPartition partition{{0, 1}, {}, fesa::Vector(0)};
heat_value::ThermalFactorization cache;
NeverCalledLinear solver;
bool fail_solve = false, fail_recovery = false, fail_commit = false;
fake->solve = [&](const auto& supplied, const auto& p, const auto& previous,
double delta, auto& backend, auto& workspace) {
EXPECT_EQ(&supplied, &system); EXPECT_EQ(&p, &partition); EXPECT_EQ(&previous, &old);
EXPECT_EQ(&backend, &solver); EXPECT_EQ(&workspace, &cache); EXPECT_DOUBLE_EQ(delta, 17);
EXPECT_EQ(&supplied.k, &k); EXPECT_EQ(&supplied.c, &c); EXPECT_EQ(&supplied.q, &q);
EXPECT_EQ(supplied.k_error, nullptr); EXPECT_EQ(supplied.c_error, nullptr);
EXPECT_EQ(supplied.q_error, nullptr); EXPECT_EQ(p.free, (std::vector<std::size_t>{0, 1}));
EXPECT_TRUE(p.constrained.empty()); EXPECT_EQ(p.prescribed.Size(), 0U);
if (fail_solve)
return fesa::Result<heat_value::ThermalStepCandidate>::Failure(InjectionFailure());
heat_value::ThermalStepCandidate pending{fesa::Vector(2, 21), fesa::Vector(2, 22),
fesa::Vector(2, 23), fesa::Vector(2, 24),
{31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, true, 42, 43}};
return fesa::Result<heat_value::ThermalStepCandidate>::Success(std::move(pending));
};
const heat_value::ThermalRecovery recovery = [&](const fesa::Vector& temperature) {
events.push_back("recovery"); ExpectValueVector(temperature, 21);
if (fail_recovery)
return fesa::Result<std::vector<fesa::ThermalElementFlux>>::Failure(InjectionFailure());
std::vector<fesa::ThermalElementFlux> flux(1);
for (auto& value : flux[0]) value = fesa::Vector3{51, 52, 53};
return fesa::Result<std::vector<fesa::ThermalElementFlux>>::Success(std::move(flux));
};
std::vector<heat_value::ThermalStepCandidate> history;
const auto commit = [&](heat_value::ThermalStepCandidate&& pending,
std::vector<fesa::ThermalElementFlux>&& flux) {
events.push_back("commit");
ExpectValueVector(pending.temperature, 21); ExpectValueVector(pending.residual, 22);
ExpectValueVector(pending.reaction, 23); ExpectValueVector(pending.certified_residual, 24);
const auto& e = pending.evidence;
EXPECT_DOUBLE_EQ(e.beta_cert, 31); EXPECT_DOUBLE_EQ(e.beta_limit, 32);
EXPECT_DOUBLE_EQ(e.free_residual_cert, 33); EXPECT_DOUBLE_EQ(e.full_free_residual_cert, 34);
EXPECT_DOUBLE_EQ(e.full_beta_cert, 35); EXPECT_DOUBLE_EQ(e.operator_construction_error, 36);
EXPECT_DOUBLE_EQ(e.rhs_construction_error, 37); EXPECT_DOUBLE_EQ(e.construction_identity_bound, 38);
EXPECT_DOUBLE_EQ(e.energy_change, 39); EXPECT_DOUBLE_EQ(e.energy_defect, 40);
EXPECT_DOUBLE_EQ(e.energy_bound, 41); EXPECT_TRUE(e.dissipation_checked);
EXPECT_DOUBLE_EQ(e.dissipation_defect, 42); EXPECT_DOUBLE_EQ(e.dissipation_bound, 43);
EXPECT_EQ(flux.size(), 1U);
for (const auto& value : flux[0]) EXPECT_EQ(value.Components(), (std::array<double, 3>{51, 52, 53}));
if (fail_commit) return InjectionFailure();
history.push_back(std::move(pending)); return fesa::Status::Ok();
};
ASSERT_TRUE(heat_value::RunThermalStep(*integrator, system, partition, old,
17, solver, cache, recovery, commit).IsOk());
EXPECT_EQ(events, (std::vector<std::string>{"solve", "recovery", "commit"}));
ASSERT_EQ(history.size(), 1U);
events.clear(); fail_solve = true;
ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition,
old, 17, solver, cache, recovery, commit));
EXPECT_EQ(events, (std::vector<std::string>{"solve"})); EXPECT_EQ(history.size(), 1U);
events.clear(); fail_solve = false; fail_recovery = true;
ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition,
old, 17, solver, cache, recovery, commit));
EXPECT_EQ(events, (std::vector<std::string>{"solve", "recovery"})); EXPECT_EQ(history.size(), 1U);
events.clear(); fail_recovery = false; fail_commit = true;
ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition,
old, 17, solver, cache, recovery, commit));
EXPECT_EQ(events, (std::vector<std::string>{"solve", "recovery", "commit"}));
EXPECT_EQ(history.size(), 1U); ExpectValueVector(old, 1);
ExpectValueVector(history[0].temperature, 21); EXPECT_FALSE(cache.matrix.has_value());
EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed);
}
+67 -33
View File
@@ -6,7 +6,6 @@
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>
@@ -93,38 +92,6 @@ TEST(EigenReduction, RemovesOnlyActualFreeDirectorNull) {
.HasValue());
}
TEST(EigenReduction, RejectsNonfiniteUsedBasisAndPreservesPriorCandidate) {
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
const auto mass = NodalMass(frame.director);
fesa::Matrix stiffness(6U, 6U);
for (std::size_t i = 0; i < 5U; ++i) stiffness(i, i) = mass(i, i) * 7.0;
stiffness(5U, 5U) = .25;
const auto matrix = Sparse(stiffness);
const std::vector<std::uint8_t> mask(6U, 0U);
const auto prior = fesa::eigen_internal::BuildEigenReduction(
matrix, {mass}, {frame}, mask, {.25}, 2.0);
ASSERT_TRUE(prior.HasValue());
const auto prior_values = prior.Value().whitened_operator.Values();
for (const double invalid : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity()}) {
for (const bool first_tangent : {false, true}) {
auto bad_frame = frame;
(first_tangent ? bad_frame.tangent_a : bad_frame.tangent_b)[0] = invalid;
const auto rejected = fesa::eigen_internal::BuildEigenReduction(
matrix, {mass}, {bad_frame}, mask, {.25}, 2.0);
ASSERT_FALSE(rejected.HasValue());
EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kSolver);
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code,
"invalid-eigen-mass-space");
EXPECT_EQ(prior.Value().whitened_operator.Values(), prior_values);
}
}
const auto rerun = fesa::eigen_internal::BuildEigenReduction(
matrix, {mass}, {frame}, mask, {.25}, 2.0);
ASSERT_TRUE(rerun.HasValue());
EXPECT_EQ(rerun.Value().whitened_operator.Values(), prior_values);
}
TEST(EigenReduction, RejectsPositiveButUnresolvableMassWithoutDeletingDofs) {
const double p = 1e-8;
const std::array<double, 3> d{2 * p / (1 + p * p), 0,
@@ -215,3 +182,70 @@ TEST(EigenReduction, HandlesTwoFreeRotationsAndRejectsEmptyFiniteSpace) {
EXPECT_EQ(empty.GetStatus().Diagnostics()[0].code, "empty-eigenproblem");
}
}
TEST(EigenReduction, PureMassWhiteningRetainsInclusiveInfinityConditionGate) {
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
for (const double diagonal : {0x1p-31, 0x1p-32, 0x1p-33}) {
auto mass = NodalMass(frame.director);
mass(0, 0) = 1.0;
mass(1, 1) = diagonal;
fesa::Matrix stiffness{6U, 6U};
stiffness(0, 0) = 3.0;
stiffness(1, 1) = 3.0 * diagonal;
const auto reduced = fesa::eigen_internal::BuildEigenReduction(
Sparse(stiffness), {mass}, {frame}, {0, 0, 1, 1, 1, 1}, {0.25}, 1.0);
EXPECT_EQ(reduced.HasValue(), diagonal >= 0x1p-32);
if (reduced.HasValue()) {
EXPECT_DOUBLE_EQ(reduced.Value().nodes[0].record.rcond_inf, diagonal);
fesa::Vector one{2U, 1.0};
const auto applied = reduced.Value().whitened_operator.Multiply(one);
EXPECT_NEAR(applied[0], 3.0, 1e-12);
EXPECT_NEAR(applied[1], 3.0, 1e-12);
} else {
EXPECT_EQ(reduced.GetStatus().Diagnostics()[0].code,
"eigen-mass-conditioning");
}
}
}
TEST(EigenReduction, PureOffDiagonalWhiteningMatchesAnalyticGramAndPencil) {
const fesa::ShellNodeInitialFrame frame{0U, {0, 0, 1}, {1, 0, 0}, {0, 1, 0}};
auto mass = NodalMass(frame.director);
mass(0, 1) = mass(1, 0) = 1.0;
fesa::Matrix stiffness{6U, 6U};
stiffness(0, 0) = stiffness(1, 1) = 6.0;
stiffness(0, 1) = stiffness(1, 0) = 4.0;
// For M=[[2,1],[1,2]], physical whitening columns are analytically
// (1/sqrt(2),0) and (-1/sqrt(6),2/sqrt(6)). No eigensolver is invoked.
for (const double length : {0.25, 3.0, 16.0}) {
const auto reduced = fesa::eigen_internal::BuildEigenReduction(
Sparse(stiffness), {mass}, {frame}, {0, 0, 1, 1, 1, 1}, {0.25}, length);
ASSERT_TRUE(reduced.HasValue());
fesa::Matrix canonical{2U, 2U};
canonical(0, 0) = canonical(1, 1) = 1.0;
std::array<fesa::Vector, 2> physical{fesa::Vector{6U}, fesa::Vector{6U}};
for (std::size_t column = 0; column < 2U; ++column) {
physical[column] = fesa::eigen_internal::RestoreScaledMode(
reduced.Value(), canonical, column);
for (std::size_t i = 0; i < 3U; ++i) physical[column][i] *= length;
}
EXPECT_NEAR(physical[0][0], 1.0 / std::sqrt(2.0), 1e-12);
EXPECT_NEAR(physical[0][1], 0.0, 1e-12);
EXPECT_NEAR(physical[1][0], -1.0 / std::sqrt(6.0), 1e-12);
EXPECT_NEAR(physical[1][1], 2.0 / std::sqrt(6.0), 1e-12);
const double expected[2][2]{{3.0, 1.0 / std::sqrt(3.0)},
{1.0 / std::sqrt(3.0), 7.0 / 3.0}};
for (std::size_t column = 0; column < 2U; ++column) {
fesa::Vector coordinate{2U};
coordinate[column] = 1.0;
const auto action = reduced.Value().whitened_operator.Multiply(coordinate);
const auto mq = mass.Multiply(physical[column]);
const auto kq = stiffness.Multiply(physical[column]);
for (std::size_t row = 0; row < 2U; ++row) {
EXPECT_NEAR(physical[row].Dot(mq), row == column ? 1.0 : 0.0, 1e-12);
EXPECT_NEAR(action[row], expected[row][column], 1e-12);
EXPECT_NEAR(action[row], physical[row].Dot(kq), 1e-12);
}
}
}
}
@@ -5,8 +5,6 @@
#include <cmath>
#include <cfenv>
#include <cstddef>
#include <cstring>
#include <functional>
#include <iostream>
#include <memory>
#include <limits>
@@ -21,7 +19,6 @@
#endif
#include "analysis/hht_reduced_internal.h"
#include "math/dense_blas_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/hht_time_integration.h"
#include "fesa/assembly/parallel_for.h"
@@ -29,71 +26,6 @@
namespace {
class RestoreTrialEnvironment {
public:
RestoreTrialEnvironment()
: rounding_(std::fegetround()),
dynamic_(mkl_get_dynamic()),
tls_(mkl_set_num_threads_local(0)) {
#if defined(_M_X64)
mxcsr_ = _mm_getcsr();
_mm_setcsr((_mm_getcsr() | 0x1F80U) & ~0x8040U);
#endif
static_cast<void>(std::fesetround(FE_TONEAREST));
mkl_set_dynamic(0);
}
~RestoreTrialEnvironment() {
mkl_set_dynamic(dynamic_);
static_cast<void>(mkl_set_num_threads_local(tls_));
static_cast<void>(std::fesetround(rounding_));
#if defined(_M_X64)
_mm_setcsr(mxcsr_);
#endif
}
private:
int rounding_;
int dynamic_;
int tls_;
#if defined(_M_X64)
unsigned int mxcsr_;
#endif
};
void ExpectSameBits(const double left, const double right) {
EXPECT_EQ(std::memcmp(&left, &right, sizeof(double)), 0);
}
void ExpectSameOperators(
const fesa::hht_reduced_internal::ConstraintOperators& left,
const fesa::hht_reduced_internal::ConstraintOperators& right) {
ASSERT_EQ(left.rows.size(), right.rows.size());
for (std::size_t row = 0U; row < left.rows.size(); ++row) {
const auto& a = left.rows[row];
const auto& b = right.rows[row];
ExpectSameBits(a.value, b.value);
ExpectSameBits(a.value_scale, b.value_scale);
ExpectSameBits(a.time_first, b.time_first);
ExpectSameBits(a.time_second, b.time_second);
for (const auto& pair : {std::make_pair(&a.gradient, &b.gradient),
std::make_pair(&a.time_gradient, &b.time_gradient),
std::make_pair(&a.third_directional_gradient,
&b.third_directional_gradient)}) {
ASSERT_EQ(pair.first->size(), pair.second->size());
for (std::size_t i = 0U; i < pair.first->size(); ++i) {
EXPECT_EQ((*pair.first)[i].dof, (*pair.second)[i].dof);
ExpectSameBits((*pair.first)[i].value, (*pair.second)[i].value);
}
}
ASSERT_EQ(a.hessian.size(), b.hessian.size());
for (std::size_t i = 0U; i < a.hessian.size(); ++i) {
EXPECT_EQ(a.hessian[i].row, b.hessian[i].row);
EXPECT_EQ(a.hessian[i].column, b.hessian[i].column);
ExpectSameBits(a.hessian[i].value, b.hessian[i].value);
}
}
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DofManager dofs;
@@ -462,480 +394,6 @@ TEST(HhtReducedChart, InvertibleGraphDoesNotCertifySelectorTransversality) {
assembly, chart.Value(), solver).HasValue());
}
TEST(HhtReducedOperators,
CompleteTrialConstraintDataMatchesFreshAndOwnsCopies) {
RestoreTrialEnvironment restore;
const auto fixture = MakeFixture(false, true);
const auto shells =
fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
auto q = zero;
auto v = zero;
auto force = zero;
auto rate = zero;
auto acceleration = zero;
for (std::size_t i = 0U; i < zero.Size(); ++i) {
q[i] = 0.001 * static_cast<double>((i % 5U) + 1U);
v[i] = 0.002 * static_cast<double>((i % 7U) + 1U);
if (i % 6U >= 3U) {
force[i] = 0.03;
rate[i] = -0.02;
acceleration[i] = 0.01;
}
}
auto assembled = Assemble(fixture, shells.Value(), q, v, zero, force, true);
ASSERT_TRUE(assembled.HasValue());
ASSERT_TRUE(assembled.Value().trial_constraints.has_value());
ASSERT_EQ(assembled.Value().trial_constraints->shells.size(),
shells.Value().size());
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembled.Value().rotations);
ASSERT_TRUE(chart.HasValue());
for (std::size_t e = 0U; e < shells.Value().size(); ++e) {
const auto& saved = assembled.Value().trial_constraints->shells[e];
ASSERT_TRUE(saved.directional_internal);
fesa::NonlinearMitc4TrialDofs trial{};
fesa::Vector direction{24U};
for (std::size_t node = 0U; node < 4U; ++node) {
const auto first = 6U * node;
trial.displacement[node] =
fesa::Vector3{saved.q[first], saved.q[first + 1U], saved.q[first + 2U]};
trial.rotation_vector[node] = fesa::Vector3{
saved.q[first + 3U], saved.q[first + 4U], saved.q[first + 5U]};
}
for (std::size_t i = 0U; i < 24U; ++i) direction[i] = saved.v[i];
const auto response = fesa::EvaluateInternalResidualAndTangent(
shells.Value()[e].internal, trial, &direction);
ASSERT_TRUE(response.HasValue());
for (std::size_t node = 0U; node < 4U; ++node) {
ExpectSameBits(saved.drilling_constraint[node],
response.Value().drilling_constraint[node]);
for (std::size_t i = 0U; i < 24U; ++i) {
ExpectSameBits(saved.drilling_constraint_gradient(node, i),
response.Value().drilling_constraint_gradient(node, i));
ExpectSameBits(
saved.drilling_constraint_third_directional_gradient[node][i],
response.Value()
.drilling_constraint_third_directional_gradient[node][i]);
for (std::size_t j = 0U; j < 24U; ++j)
ExpectSameBits(
saved.drilling_constraint_hessian[node](i, j),
response.Value().drilling_constraint_hessian[node](i, j));
}
}
}
const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) {
return fesa::hht_reduced_internal::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(),
rate, acceleration);
};
auto fresh = assembled.Value();
fresh.trial_constraints.reset();
const auto reference = evaluate(fresh);
const auto reused = evaluate(assembled.Value());
ASSERT_TRUE(reference.HasValue());
ASSERT_TRUE(reused.HasValue());
ExpectSameOperators(reused.Value(), reference.Value());
auto copy = assembled.Value();
copy.trial_constraints->shells[0U].drilling_constraint[0U] += 7.0;
// Test-only finite marker proves the selected route actually reads its owner.
const auto marker = evaluate(copy);
ASSERT_TRUE(marker.HasValue());
EXPECT_NE(marker.Value().rows[0U].value, reused.Value().rows[0U].value);
EXPECT_NE(
copy.trial_constraints->shells[0U].drilling_constraint[0U],
assembled.Value().trial_constraints->shells[0U].drilling_constraint[0U]);
auto moved = std::move(assembled.Value());
const auto moved_result = evaluate(moved);
ASSERT_TRUE(moved_result.HasValue());
ExpectSameOperators(moved_result.Value(), reference.Value());
fesa::DynamicAcceptedFrameState frame{};
frame.generalized_coordinates = q;
frame.velocity = v;
frame.acceleration = zero;
frame.algorithmic_acceleration = zero;
frame.external_force = force;
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
fesa::TbbParallelFor parallel;
const auto tbb = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), frame, zero, 0.1,
parameters.Value(), parallel, true);
ASSERT_TRUE(tbb.HasValue());
const auto tbb_result = evaluate(tbb.Value());
ASSERT_TRUE(tbb_result.HasValue());
ExpectSameOperators(tbb_result.Value(), reference.Value());
}
TEST(HhtReducedOperators,
ChangedTrialConstraintContextFallsBackInOriginalPosition) {
RestoreTrialEnvironment restore;
const auto fixture = MakeFixture(false);
const auto shells =
fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto assembled =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
ASSERT_TRUE(assembled.HasValue());
ASSERT_TRUE(assembled.Value().trial_constraints.has_value());
ASSERT_TRUE(
assembled.Value().trial_constraints->shells[0U].directional_internal);
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembled.Value().rotations);
ASSERT_TRUE(chart.HasValue());
const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) {
return fesa::hht_reduced_internal::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(),
zero, zero);
};
using Assembly = fesa::hht_internal::DynamicAssembly;
std::vector<std::function<void(Assembly&)>> changes{
[](Assembly& a) { a.frame.generalized_coordinates[2U] = 0.001; },
[](Assembly& a) { a.frame.velocity[3U] = 0.003; },
[](Assembly& a) { a.frame.time = 0.1; },
[](Assembly& a) { ++a.frame.frame_index; },
[](Assembly& a) { ++a.frame.increment_index; },
[](Assembly& a) { ++a.frame.iteration_count; },
[](Assembly& a) { a.frame.is_initial = true; },
[](Assembly& a) { a.trial_constraints->source_path = "different.inp"; },
[](Assembly& a) { ++a.trial_constraints->shells[0U].element_index; },
[](Assembly& a) { ++a.trial_constraints->shells[0U].scatter[0U]; },
[](Assembly& a) {
a.trial_constraints->shells[0U].directional_internal = false;
},
[](Assembly& a) {
a.trial_constraints->shells[0U].node_source_ids[0U].instance_name +=
"x";
},
[](Assembly& a) {
++a.trial_constraints->shells[0U].node_source_ids[0U].source_label;
},
[](Assembly& a) {
a.trial_constraints->shells[0U].node_source_ids[0U].source_label_text +=
"x";
},
[](Assembly& a) {
++a.trial_constraints->shells[0U].configuration.source.line;
},
[](Assembly& a) {
a.trial_constraints->shells[0U].configuration.source.file =
"different.inp";
},
[](Assembly& a) {
a.trial_constraints->shells[0U].configuration.identity += "x";
},
[](Assembly& a) { a.trial_constraints->shells[0U].q[0U] = -0.0; },
[](Assembly& a) { a.trial_constraints->shells[0U].v[0U] = -0.0; },
[](Assembly& a) {
a.trial_constraints->shells[0U].drilling_constraint_gradient =
fesa::Matrix{0U, 0U};
},
[](Assembly& a) {
a.trial_constraints->shells[0U].drilling_constraint_hessian[2U] =
fesa::Matrix{0U, 0U};
},
[](Assembly& a) {
a.trial_constraints->shells[0U]
.drilling_constraint_third_directional_gradient[1U] =
fesa::Vector{0U};
},
[](Assembly& a) {
a.trial_constraints->shells[0U].drilling_constraint_hessian[0U](
0U, 0U) = std::numeric_limits<double>::infinity();
},
[](Assembly& a) {
a.trial_constraints->shells[0U]
.evaluation_context.dense_policy.blas_max_threads = 2;
},
[](Assembly& a) {
a.trial_constraints->shells[0U]
.evaluation_context.dense_policy.dynamic = 1;
},
[](Assembly& a) {
a.trial_constraints->shells[0U]
.evaluation_context.dense_policy.dynamic = -1;
},
[](Assembly& a) {
a.trial_constraints->shells[0U].evaluation_context.mxcsr_control ^=
0x8000U;
},
[](Assembly& a) {
a.trial_constraints->shells[0U].evaluation_context.supported = false;
}};
for (std::size_t i = 0U; i < 64U; ++i)
changes.push_back([i](Assembly& a) {
a.trial_constraints->shells[0U].configuration.values[i] += 0.125;
});
for (const double invalid : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity()})
changes.push_back([invalid](Assembly& a) {
a.trial_constraints->shells[0U].configuration.values[60U] = invalid;
});
for (std::size_t i = 0U; i < changes.size(); ++i) {
SCOPED_TRACE(i);
auto candidate = assembled.Value();
candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0;
changes[i](candidate);
auto fresh = candidate;
fresh.trial_constraints.reset();
const auto reference = evaluate(fresh);
const auto actual = evaluate(candidate);
ASSERT_TRUE(reference.HasValue());
ASSERT_TRUE(actual.HasValue());
ExpectSameOperators(actual.Value(), reference.Value());
}
for (std::size_t kind = 0U; kind < 2U; ++kind) {
auto candidate = assembled.Value();
if (kind == 0U) candidate.trial_constraints->shells.clear();
if (kind == 1U) candidate.trial_constraints.reset();
auto fresh = assembled.Value();
fresh.trial_constraints.reset();
const auto reference = evaluate(fresh);
const auto actual = evaluate(candidate);
ASSERT_TRUE(reference.HasValue());
ASSERT_TRUE(actual.HasValue());
ExpectSameOperators(actual.Value(), reference.Value());
}
// Change the actual consumer environment, preserving the original fallback.
for (std::size_t mode = 0U; mode < 5U; ++mode) {
SCOPED_TRACE(mode);
auto candidate = assembled.Value();
candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0;
auto fresh = candidate;
fresh.trial_constraints.reset();
#if defined(_M_X64)
const auto csr = _mm_getcsr();
if (mode == 0U) _mm_setcsr(csr ^ 0x8000U);
if (mode == 1U) _mm_setcsr(csr ^ 0x0040U);
#endif
const int rounding = std::fegetround();
if (mode == 2U) static_cast<void>(std::fesetround(FE_DOWNWARD));
if (mode == 3U) static_cast<void>(mkl_set_num_threads_local(2));
if (mode == 4U) mkl_set_dynamic(1);
const auto reference = evaluate(fresh);
const auto actual = evaluate(candidate);
if (mode == 4U) mkl_set_dynamic(0);
if (mode == 3U) EXPECT_EQ(mkl_set_num_threads_local(0), 2);
static_cast<void>(std::fesetround(rounding));
#if defined(_M_X64)
_mm_setcsr(csr);
#endif
ASSERT_TRUE(reference.HasValue());
ASSERT_TRUE(actual.HasValue());
ExpectSameOperators(actual.Value(), reference.Value());
}
auto failed_trial = assembled.Value();
failed_trial.frame.generalized_coordinates[0U] =
std::numeric_limits<double>::infinity();
auto fresh_failure = failed_trial;
fresh_failure.trial_constraints.reset();
const auto failed = evaluate(failed_trial);
const auto original = evaluate(fresh_failure);
ASSERT_FALSE(failed.HasValue());
ASSERT_FALSE(original.HasValue());
ASSERT_EQ(failed.GetStatus().Diagnostics().size(),
original.GetStatus().Diagnostics().size());
EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].code,
original.GetStatus().Diagnostics()[0U].code);
EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].message,
original.GetStatus().Diagnostics()[0U].message);
const auto rerun = evaluate(assembled.Value());
auto fresh = assembled.Value();
fresh.trial_constraints.reset();
const auto reference = evaluate(fresh);
ASSERT_TRUE(rerun.HasValue());
ASSERT_TRUE(reference.HasValue());
ExpectSameOperators(rerun.Value(), reference.Value());
}
TEST(HhtReducedOperators, TrialContextControlsAndInitialFalseFailureLifetime) {
RestoreTrialEnvironment restore;
const auto context = fesa::hht_internal::ReadTrialEvaluationContext();
ASSERT_TRUE(fesa::hht_internal::ReusableTrialContext(context, context));
for (const unsigned int bit : {0x0040U, 0x8000U, 0x2000U, 0x0080U}) {
auto changed = context;
changed.mxcsr_control ^= bit;
EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(context, changed));
}
auto unmasked = context;
unmasked.mxcsr_control &= ~0x0080U;
EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(unmasked, unmasked));
static_cast<void>(mkl_set_num_threads_local(2));
const auto positive = fesa::hht_internal::ReadTrialEvaluationContext();
EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(positive, positive));
EXPECT_EQ(mkl_set_num_threads_local(0), 2);
mkl_set_dynamic(1);
const auto dynamic = fesa::hht_internal::ReadTrialEvaluationContext();
EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(dynamic, dynamic));
mkl_set_dynamic(0);
const auto fixture = MakeFixture(false);
const auto shells =
fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto position =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, false);
ASSERT_TRUE(position.HasValue());
EXPECT_FALSE(position.Value().trial_constraints.has_value());
mkl_set_dynamic(1);
const auto dynamic_trial =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
mkl_set_dynamic(0);
ASSERT_TRUE(dynamic_trial.HasValue());
ASSERT_TRUE(dynamic_trial.Value().trial_constraints.has_value());
EXPECT_FALSE(
dynamic_trial.Value().trial_constraints->shells[0U].directional_internal);
static_cast<void>(mkl_set_num_threads_local(2));
const auto positive_trial =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
EXPECT_EQ(mkl_set_num_threads_local(0), 2);
ASSERT_TRUE(positive_trial.HasValue());
ASSERT_TRUE(positive_trial.Value().trial_constraints.has_value());
EXPECT_FALSE(positive_trial.Value()
.trial_constraints->shells[0U]
.directional_internal);
fesa::DynamicAcceptedFrameState frame{};
frame.is_initial = true;
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.external_force})
*vector = zero;
fesa::SerialParallelFor parallel;
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
const auto initial = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), frame, zero, 0.1,
parameters.Value(), parallel, true);
ASSERT_TRUE(initial.HasValue());
EXPECT_FALSE(initial.Value().trial_constraints.has_value());
auto invalid = zero;
invalid[0U] = std::numeric_limits<double>::quiet_NaN();
const auto failure =
Assemble(fixture, shells.Value(), invalid, zero, zero, zero, true);
EXPECT_FALSE(failure.HasValue());
const auto current =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
const auto next =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
ASSERT_TRUE(current.HasValue());
ASSERT_TRUE(next.HasValue());
ASSERT_EQ(current.Value().trial_constraints->shells.size(),
shells.Value().size());
ASSERT_EQ(next.Value().trial_constraints->shells.size(),
shells.Value().size());
EXPECT_NE(&current.Value().trial_constraints->shells[0U],
&next.Value().trial_constraints->shells[0U]);
}
TEST(HhtReducedOperators, NonfiniteMatchingTrialKeysPreserveOriginalFailure) {
RestoreTrialEnvironment restore;
const auto fixture = MakeFixture(false);
const auto shells =
fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto assembled =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
ASSERT_TRUE(assembled.HasValue());
ASSERT_TRUE(assembled.Value().trial_constraints.has_value());
ASSERT_TRUE(
assembled.Value().trial_constraints->shells[0U].directional_internal);
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembled.Value().rotations);
ASSERT_TRUE(chart.HasValue());
const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) {
return fesa::hht_reduced_internal::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(),
zero, zero);
};
const std::array<double, 3> invalid{std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity()};
for (std::size_t channel = 0U; channel < 2U; ++channel)
for (std::size_t i = 0U; i < invalid.size(); ++i) {
SCOPED_TRACE(channel);
SCOPED_TRACE(i);
auto candidate = assembled.Value();
auto& saved = candidate.trial_constraints->shells[0U];
if (channel == 0U) {
candidate.frame.generalized_coordinates[0U] = invalid[i];
saved.q[0U] = invalid[i];
} else {
candidate.frame.velocity[0U] = invalid[i];
saved.v[0U] = invalid[i];
}
auto fresh = candidate;
fresh.trial_constraints.reset();
const auto original = evaluate(fresh);
const auto actual = evaluate(candidate);
ASSERT_FALSE(original.HasValue());
ASSERT_FALSE(actual.HasValue());
EXPECT_EQ(actual.GetStatus().Category(), original.GetStatus().Category());
const auto& a = actual.GetStatus().Diagnostics();
const auto& b = original.GetStatus().Diagnostics();
ASSERT_EQ(a.size(), b.size());
for (std::size_t j = 0U; j < a.size(); ++j) {
EXPECT_EQ(a[j].severity, b[j].severity);
EXPECT_EQ(a[j].code, b[j].code);
EXPECT_EQ(a[j].location.file, b[j].location.file);
EXPECT_EQ(a[j].location.line, b[j].location.line);
EXPECT_EQ(a[j].keyword, b[j].keyword);
EXPECT_EQ(a[j].entity_identity, b[j].entity_identity);
EXPECT_EQ(a[j].message, b[j].message);
}
}
}
TEST(HhtReducedOperators, NonfiniteMatchingTimeKeyMissPreservesFreshSuccess) {
RestoreTrialEnvironment restore;
const auto fixture = MakeFixture(false);
const auto shells =
fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto assembled =
Assemble(fixture, shells.Value(), zero, zero, zero, zero, true);
ASSERT_TRUE(assembled.HasValue());
ASSERT_TRUE(assembled.Value().trial_constraints.has_value());
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembled.Value().rotations);
ASSERT_TRUE(chart.HasValue());
const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) {
return fesa::hht_reduced_internal::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(),
zero, zero);
};
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
fesa::SerialParallelFor parallel;
for (const double bad_time : {std::numeric_limits<double>::quiet_NaN(),
std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity()}) {
auto candidate = assembled.Value();
candidate.frame.time = bad_time;
candidate.trial_constraints->time = bad_time;
candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0;
auto fresh = candidate;
fresh.trial_constraints.reset();
const auto reference = evaluate(fresh);
const auto actual = evaluate(candidate);
ASSERT_TRUE(reference.HasValue());
ASSERT_TRUE(actual.HasValue());
ExpectSameOperators(actual.Value(), reference.Value());
const auto producer = fesa::hht_internal::EvaluateAssembly(
*fixture.domain, fixture.dofs, shells.Value(), candidate.frame, zero,
0.1, parameters.Value(), parallel, true);
ASSERT_TRUE(producer.HasValue());
EXPECT_FALSE(producer.Value().trial_constraints.has_value());
const auto producer_result = evaluate(producer.Value());
ASSERT_TRUE(producer_result.HasValue());
ExpectSameOperators(producer_result.Value(), reference.Value());
}
}
TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
@@ -1683,58 +1141,4 @@ TEST(HhtReducedChannelNewton, RestrictedSingularityIsNotFullInvertibility) {
EXPECT_FALSE(solver.FactorizeFresh(restricted.Value().tangent).IsOk());
}
TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero,
zero, false);
ASSERT_TRUE(assembled.HasValue());
auto assembly = assembled.Value();
const double maximum = (std::numeric_limits<double>::max)();
const double epsilon = std::ldexp(1.0, -26);
const double t = std::sqrt(1.0 - epsilon * epsilon);
const std::array<double, 3U> n{epsilon, 0.6 * t, 0.8 * t};
const std::array<double, 9U> block{0.0, maximum, maximum,
maximum, 0.0, 0.0,
maximum, 0.0, 0.0};
double original_metric = 0.0;
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
original_metric += (n[i] * block[3U * i + j]) * n[j];
ASSERT_TRUE(std::isfinite(original_metric));
ASSERT_GT(original_metric, 0.0);
ASSERT_FALSE(std::isfinite(block[1U] * n[1U] + block[2U] * n[2U]));
std::vector<fesa::CooContribution> entries;
for (std::size_t node = 0U; node < assembly.rotations.size(); ++node) {
assembly.rotations[node].drill_null = fesa::Vector3(n);
for (std::size_t i = 0U; i < 3U; ++i)
for (std::size_t j = 0U; j < 3U; ++j)
entries.push_back({6U * node + 3U + i, 6U * node + 3U + j,
block[3U * i + j], node, 3U * i + j});
}
const auto metric = fesa::hht_internal::MakeSparse(
fixture.dofs.FullDofCount(), fixture.dofs.FullDofCount(),
std::move(entries));
ASSERT_TRUE(metric.HasValue());
assembly.drilling_metric = metric.Value();
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
fixture.dofs, assembly.rotations);
ASSERT_TRUE(chart.HasValue());
fesa::hht_reduced_internal::ConstraintOperators operators;
operators.rows.resize(chart.Value().null_nodes.size());
for (std::size_t row = 0U; row < operators.rows.size(); ++row)
for (std::size_t component = 0U; component < 3U; ++component)
operators.rows[row].gradient.push_back(
{6U * chart.Value().null_nodes[row] + 3U + component,
original_metric * n[component]});
fesa::MklPardisoTangentSolver solver;
const auto checked = fesa::hht_reduced_internal::CheckGraphRegularity(
assembly, chart.Value(), operators, solver);
ASSERT_TRUE(checked.HasValue());
EXPECT_GE(checked.Value(), std::ldexp(1.0, -32));
}
} // namespace
@@ -573,8 +573,7 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) {
const fesa::Vector3 velocity{1.0, 0.0, 1.0};
const fesa::Vector3 acceleration{0.0, -1.0 / 3.0, 0.0};
const fesa::Vector3 force{1.0, 0.0, 0.0};
const auto external = fixture.shells[0U].internal
.EvaluateExternalDrillingConstraint(
const auto external = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, phi, velocity, force, {}, {});
ASSERT_TRUE(external.HasValue());
double gradient_acceleration = 0.0;
@@ -586,14 +585,11 @@ TEST(HhtShellPhysics, ExternalDrillingHessianBalancesGradientAcceleration) {
EXPECT_NEAR(external.Value().directional_second + gradient_acceleration,
0.0, 1.0e-14);
constexpr double step = 1.0e-4;
const auto plus = fixture.shells[0U].internal
.EvaluateExternalDrillingConstraint(
const auto plus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, {step, 0.0, step}, {}, force, {}, {});
const auto middle = fixture.shells[0U].internal
.EvaluateExternalDrillingConstraint(
const auto middle = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, phi, {}, force, {}, {});
const auto minus = fixture.shells[0U].internal
.EvaluateExternalDrillingConstraint(
const auto minus = fixture.shells[0U].internal->EvaluateExternalDrillingConstraint(
0U, {-step, 0.0, -step}, {}, force, {}, {});
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(middle.HasValue());
@@ -21,6 +21,8 @@
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "solid_hht_test_fixture.h"
#include "fesa/analysis/analysis_model.h"
namespace {
fesa::Result<fesa::SolidDynamicExecutionResult> RunDynamics(
fesa::ModelDefinition d) {
@@ -1,4 +1,7 @@
#include "fesa/analysis/solid_static_procedure.h"
#include "analysis/solid_static_core.h"
#include "analysis/equilibrium_algorithm.h"
#include "fesa/core/source_identity.h"
#include <gtest/gtest.h>
@@ -7,6 +10,7 @@
#include <fstream>
#include <iostream>
#include <limits>
#include <memory>
#include <sstream>
#include <string>
@@ -824,3 +828,116 @@ TEST(SolidStaticProcedure,
<< " energy=" << frame.physical_strain_energy << '\n';
}
}
namespace {
fesa::Status StaticPolicyFailure() {
return fesa::Status::Failure(fesa::FailureCategory::kSolver,
{{fesa::Severity::kError, "injected-static-algorithm", {}, "STATIC",
"original-owner", "Original algorithm failure."}});
}
class UnusedStaticSolvers final : public fesa::LinearSolver,
public fesa::TangentSolver {
public:
mutable int calls = 0;
fesa::Status Factorize(const fesa::SparseMatrix&) override {
++calls; return StaticPolicyFailure();
}
fesa::Status Solve(const fesa::Vector&, fesa::Vector&) const override {
++calls; return StaticPolicyFailure();
}
fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override {
++calls; return StaticPolicyFailure();
}
fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override {
++calls; return StaticPolicyFailure();
}
};
class RefusingStaticAlgorithm final : public fesa::EquilibriumAlgorithm {
public:
RefusingStaticAlgorithm(const fesa::SolidStaticState& state, bool& destroyed)
: state_(state), destroyed_(destroyed) {}
~RefusingStaticAlgorithm() override { destroyed_ = true; }
int calls = 0;
fesa::SolidStaticFrame baseline;
fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last,
const Evaluation&) override {
++calls;
EXPECT_EQ(first, 1U); EXPECT_EQ(last, 100U);
EXPECT_TRUE(state_.AcceptedFrames().empty());
EXPECT_TRUE(state_.IterationRecords().empty());
baseline = state_.CommittedState();
return StaticPolicyFailure();
}
private:
const fesa::SolidStaticState& state_;
bool& destroyed_;
};
} // namespace
TEST(SolidStaticProcedure, InjectedAlgorithmFailurePreservesInitializedBaseline) {
auto domain = Map(Deck(Supports(), Tension(1.0), true));
ASSERT_TRUE(domain.HasValue()) << Message(domain.GetStatus());
fesa::SerialParallelFor serial;
UnusedStaticSolvers solvers;
fesa::SolidStaticState state;
bool destroyed = false;
auto owned = std::make_unique<RefusingStaticAlgorithm>(state, destroyed);
auto* selected = owned.get();
std::unique_ptr<fesa::EquilibriumAlgorithm> algorithm = std::move(owned);
{
fesa::SolidStaticCore core(serial, solvers, solvers, *algorithm);
const auto status = core.ExecuteInto(domain.Value(), state);
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver);
ASSERT_EQ(status.Diagnostics().size(), 1U);
const auto& diagnostic = status.Diagnostics()[0];
EXPECT_EQ(diagnostic.code, "injected-static-algorithm");
EXPECT_EQ(diagnostic.keyword, "STATIC");
EXPECT_EQ(diagnostic.entity_identity, "original-owner");
EXPECT_EQ(diagnostic.message, "Original algorithm failure.");
}
EXPECT_EQ(selected->calls, 1); EXPECT_EQ(solvers.calls, 0);
EXPECT_TRUE(state.AcceptedFrames().empty());
EXPECT_TRUE(state.IterationRecords().empty());
// Core initialization legitimately installs the zero-factor baseline before
// iteration. Compare to that exact baseline at algorithm entry, not to the
// initially uninitialized observer.
const auto& expected = selected->baseline;
const auto& actual = state.CommittedState();
EXPECT_EQ(actual.frame_index, expected.frame_index);
EXPECT_EQ(actual.increment_index, expected.increment_index);
EXPECT_DOUBLE_EQ(actual.load_factor, expected.load_factor);
EXPECT_EQ(actual.iteration_count, expected.iteration_count);
EXPECT_EQ(actual.is_factor_one, expected.is_factor_one);
EXPECT_EQ(actual.nodal_internal_order, expected.nodal_internal_order);
ASSERT_EQ(actual.nodal_source_ids.size(), expected.nodal_source_ids.size());
for (std::size_t i = 0; i < actual.nodal_source_ids.size(); ++i)
EXPECT_TRUE(fesa::SourceIdentityEquals(actual.nodal_source_ids[i], expected.nodal_source_ids[i]));
const auto same_vector = [](const fesa::Vector& a, const fesa::Vector& b) {
ASSERT_EQ(a.Size(), b.Size());
for (std::size_t i = 0; i < a.Size(); ++i) EXPECT_DOUBLE_EQ(a[i], b[i]);
};
same_vector(actual.displacement, expected.displacement);
same_vector(actual.internal_force, expected.internal_force);
same_vector(actual.external_force, expected.external_force);
same_vector(actual.residual, expected.residual); same_vector(actual.reaction, expected.reaction);
EXPECT_DOUBLE_EQ(actual.physical_strain_energy, expected.physical_strain_energy);
EXPECT_EQ(actual.global_equilibrium, expected.global_equilibrium);
EXPECT_EQ(actual.balance.internal_force_sum, expected.balance.internal_force_sum);
EXPECT_EQ(actual.balance.reference_internal_moment, expected.balance.reference_internal_moment);
EXPECT_EQ(actual.balance.current_internal_moment, expected.balance.current_internal_moment);
EXPECT_EQ(actual.balance.free_residual_moment, expected.balance.free_residual_moment);
EXPECT_EQ(actual.balance.linearization_moment, expected.balance.linearization_moment);
EXPECT_EQ(actual.balance.internal_force_scale, expected.balance.internal_force_scale);
EXPECT_EQ(actual.balance.reference_moment_scale, expected.balance.reference_moment_scale);
EXPECT_EQ(actual.balance.current_moment_scale, expected.balance.current_moment_scale);
ASSERT_EQ(actual.solid_results.size(), expected.solid_results.size());
for (std::size_t i = 0; i < actual.solid_results.size(); ++i) {
const auto& a = actual.solid_results[i]; const auto& b = expected.solid_results[i];
EXPECT_EQ(a.element, b.element); EXPECT_EQ(a.integration_point, b.integration_point);
EXPECT_EQ(a.natural_coordinates, b.natural_coordinates);
EXPECT_EQ(a.reference_coordinates, b.reference_coordinates);
EXPECT_EQ(a.cauchy_stress, b.cauchy_stress); EXPECT_EQ(a.strain, b.strain);
}
EXPECT_FALSE(destroyed); algorithm.reset(); EXPECT_TRUE(destroyed);
}
@@ -11,6 +11,8 @@
#include <utility>
#include <vector>
#include "fesa/assembly/thermal_assembler.h"
namespace {
fesa::ModelDefinition Definition(bool constrained = true, double period = .9,
double increment = .3,
@@ -12,6 +12,8 @@
#include "analysis/thermal_transient_bounds.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/assembly/thermal_assembler.h"
namespace {
namespace ti = fesa::thermal_transient_internal;
std::string Messages(const fesa::Status& status) {
@@ -481,47 +481,3 @@ TEST(LoadAssembly, ZeroLoadsRemainZero) {
ASSERT_TRUE(constrained_rhs.HasValue());
EXPECT_EQ(constrained_rhs.Value().Size(), 0U);
}
TEST(LoadAssembly, PreservesOrderedCancellationInPrescribedCorrection) {
const std::filesystem::path source{"models/load-assembly.inp"};
auto fixture = MakeFixture(
1U, {},
{{"10", 1, 1, 1.0, {source, 21U}},
{"10", 3, 3, 1.0, {source, 22U}},
{"10", 5, 5, 1.0, {source, 23U}}},
{});
const double large = 72057594037927936.0; // 2^56, exactly representable.
const auto kfc = MakeDenseSparse(
3U, 3U, {large, 1.0, -large, large, -large, 1.0, 0.0, 0.0, 0.0});
const auto rhs = fesa::LoadAssembler::EffectiveFreeRhs(
fesa::Vector{6U}, kfc, fixture.dofs->PrescribedValues(), *fixture.dofs);
ASSERT_TRUE(rhs.HasValue());
ASSERT_EQ(rhs.Value().Size(), 3U);
EXPECT_DOUBLE_EQ(rhs.Value()[0U], 0.0);
EXPECT_DOUBLE_EQ(rhs.Value()[1U], -1.0);
EXPECT_DOUBLE_EQ(rhs.Value()[2U], 0.0);
}
TEST(LoadAssembly, SelectsFirstOrderedOverflowDiagnostic) {
const std::filesystem::path source{"models/load-assembly.inp"};
auto fixture = MakeFixture(
1U, {},
{{"10", 1, 1, 1.0, {source, 21U}},
{"10", 3, 3, 1.0, {source, 22U}},
{"10", 5, 5, 2.0, {source, 23U}}},
{});
const double maximum = (std::numeric_limits<double>::max)();
const auto kfc = MakeDenseSparse(
3U, 3U, {maximum, maximum, 0.0, 0.0, 0.0, maximum, 0.0, 0.0, 0.0});
const auto rhs = fesa::LoadAssembler::EffectiveFreeRhs(
fesa::Vector{6U}, kfc, fixture.dofs->PrescribedValues(), *fixture.dofs);
ExpectFailureCode(rhs, "nonfinite-load-accumulation");
ASSERT_EQ(rhs.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(rhs.GetStatus().Diagnostics()[0U].entity_identity, "0");
EXPECT_NE(rhs.GetStatus().Diagnostics()[0U].message.find("row sum"),
std::string::npos);
}
+63 -2
View File
@@ -185,7 +185,12 @@ class ReverseParallelFor final : public fesa::ParallelFor {
class FakeStiffnessElement final : public fesa::Element {
public:
FakeStiffnessElement(fesa::ElementDofLayout layout, fesa::Matrix stiffness)
: layout_{std::move(layout)}, stiffness_{std::move(stiffness)} {}
: layout_{std::move(layout)}, stiffness_{std::move(stiffness)},
contribution_layout_{layout_} {}
void ReturnLayout(fesa::ElementDofLayout layout) {
contribution_layout_ = std::move(layout);
}
const fesa::ElementDofLayout& DofLayout() const noexcept override {
return layout_;
@@ -194,7 +199,7 @@ class FakeStiffnessElement final : public fesa::Element {
fesa::Result<fesa::ElementStiffnessContribution> ComputeStiffness()
const override {
return fesa::Result<fesa::ElementStiffnessContribution>::Success(
{layout_, stiffness_});
{contribution_layout_, stiffness_});
}
fesa::Result<fesa::ElementResultBundle> Recover(
@@ -206,6 +211,7 @@ class FakeStiffnessElement final : public fesa::Element {
private:
fesa::ElementDofLayout layout_;
fesa::Matrix stiffness_;
fesa::ElementDofLayout contribution_layout_;
};
fesa::Matrix MatrixFromRows(const std::vector<std::vector<double>>& rows) {
@@ -414,4 +420,59 @@ TEST(SparseAssembly, S4AndS4rSemanticFixturesAssembleIdenticalStiffness) {
ExpectByteIdentical(s4r.Value(), s4.Value());
}
TEST(SparseAssembly, RejectsLayoutAndDimensionsInStableElementOrder) {
auto domain = fesa::Domain::Create(MakeDefinition());
ASSERT_TRUE(domain.HasValue());
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
const fesa::ElementDofLayout first_layout{
{"Beam-1", 10, "10"}, {0U, 1U}, {fesa::DofComponent::kUx}};
const fesa::ElementDofLayout second_layout{
{"Beam-1", 20, "20"}, {1U, 2U}, {fesa::DofComponent::kUx}};
FakeStiffnessElement first{first_layout, fesa::Matrix{2U, 2U}};
FakeStiffnessElement second{second_layout, fesa::Matrix{1U, 1U}};
const fesa::ElementView elements{std::cref(first), std::cref(second)};
fesa::DofManager dofs;
ASSERT_TRUE(dofs.Build(model.Value(), elements).IsOk());
const auto original_pattern = dofs.GetSparsePattern();
std::vector<fesa::ElementDofLayout> invalid(5U, first_layout);
invalid[0U].source_id.instance_name = "beam-1";
invalid[1U].source_id.source_label = 11;
invalid[2U].source_id.source_label_text = "010";
invalid[3U].node_indices = {1U, 0U};
invalid[4U].components_per_node = {fesa::DofComponent::kUy};
fesa::SerialParallelFor serial;
fesa::TbbParallelFor tbb;
ReverseParallelFor reverse;
for (const auto& layout : invalid) {
first.ReturnLayout(layout);
for (const fesa::ParallelFor* executor :
std::array<const fesa::ParallelFor*, 3>{&serial, &tbb, &reverse}) {
auto candidate =
fesa::SparseAssembler::Assemble(elements, dofs, *executor);
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel);
ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U);
const auto& diagnostic = candidate.GetStatus().Diagnostics()[0U];
EXPECT_EQ(diagnostic.code, "invalid-assembly-layout");
EXPECT_EQ(diagnostic.keyword, "*ELEMENT");
EXPECT_EQ(diagnostic.entity_identity, "10");
EXPECT_EQ(diagnostic.message,
"Element stiffness layout must match its declared runtime layout.");
}
}
first.ReturnLayout(first_layout);
auto candidate = fesa::SparseAssembler::Assemble(elements, dofs, reverse);
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].code,
"invalid-assembly-dimensions");
EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].entity_identity, "20");
EXPECT_EQ(first.DofLayout().node_indices, first_layout.node_indices);
EXPECT_EQ(dofs.GetSparsePattern().row_offsets, original_pattern.row_offsets);
EXPECT_EQ(dofs.GetSparsePattern().column_indices,
original_pattern.column_indices);
EXPECT_TRUE(dofs.ValidateInvariants().IsOk());
}
} // namespace
@@ -2,6 +2,7 @@
#include <cstddef>
#include <filesystem>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
@@ -185,6 +186,24 @@ TEST(EssentialConstraints, HandlesNoAllAndMixedConstraints) {
ExpectShape(all.Value().k_cc, 6U, 6U);
EXPECT_EQ(all.Value().k_cc.Values(), full.Values());
fesa::Vector full_load{6U};
for (std::size_t i = 0U; i < full_load.Size(); ++i)
full_load[i] = static_cast<double>(i) - 2.0;
auto empty_rhs = policy.EffectiveFreeRhs(
full_load, all.Value().k_fc, all_constraints.PrescribedValues(),
all_constraints);
ASSERT_TRUE(empty_rhs.HasValue());
EXPECT_EQ(empty_rhs.Value().Size(), 0U);
auto full_rhs = policy.EffectiveFreeRhs(
full_load, none.Value().k_fc, no_constraints.PrescribedValues(),
no_constraints);
ASSERT_TRUE(full_rhs.HasValue());
for (std::size_t i = 0U; i < full_load.Size(); ++i) {
EXPECT_EQ(full_rhs.Value()[i], static_cast<double>(i) - 2.0);
EXPECT_EQ(full_load[i], static_cast<double>(i) - 2.0);
}
const auto mixed_constraints = MakeDofs({{"1", 3, 4, 0.0, {{}, 12U}}});
auto mixed = policy.Partition(full, mixed_constraints);
ASSERT_TRUE(mixed.HasValue());
@@ -316,3 +335,52 @@ TEST(EssentialConstraints, RejectsDimensionOrOrderMismatch) {
fesa::Vector{4U}, fesa::Vector{2U}, dofs)),
std::invalid_argument);
}
TEST(EssentialConstraints, PreservesEliminationFailureOrderAndInputs) {
const fesa::EssentialConstraintPolicy policy;
const auto dofs = MakeDofs({{"1", 2, 2, 2.0, {{}, 12U}},
{"1", 5, 5, -1.0, {{}, 13U}}});
const double maximum = (std::numeric_limits<double>::max)();
fesa::Vector load{6U};
load[0U] = std::numeric_limits<double>::infinity();
fesa::Vector prescribed{2U};
prescribed[0U] = std::numeric_limits<double>::infinity();
prescribed[1U] = 1.0;
const auto kfc = MakeMatrix(4U, 2U, {0.4 * maximum, 0.8 * maximum,
maximum, 0.0, 0.0, 0.0, 0.0, 0.0});
const auto original_values = kfc.Values();
const auto original_pattern = dofs.GetSparsePattern();
auto candidate = policy.EffectiveFreeRhs(
load, MakeMatrix(3U, 2U, std::vector<double>(6U)), prescribed, dofs);
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].code,
"invalid-load-dimensions");
candidate = policy.EffectiveFreeRhs(load, kfc, prescribed, dofs);
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Diagnostics()[0U].entity_identity,
"full-load:0");
EXPECT_EQ(load[0U], std::numeric_limits<double>::infinity());
EXPECT_EQ(prescribed[0U], std::numeric_limits<double>::infinity());
load[0U] = 7.0;
prescribed[0U] = 2.0;
candidate = policy.EffectiveFreeRhs(load, kfc, prescribed, dofs);
ASSERT_FALSE(candidate.HasValue());
EXPECT_EQ(candidate.GetStatus().Category(), fesa::FailureCategory::kModel);
ASSERT_EQ(candidate.GetStatus().Diagnostics().size(), 1U);
const auto& diagnostic = candidate.GetStatus().Diagnostics()[0U];
EXPECT_EQ(diagnostic.code, "nonfinite-load-accumulation");
EXPECT_EQ(diagnostic.keyword, "LOAD_ASSEMBLER");
EXPECT_EQ(diagnostic.entity_identity, "0");
EXPECT_TRUE(diagnostic.location.file.empty());
EXPECT_EQ(diagnostic.location.line, 0U);
EXPECT_EQ(diagnostic.message,
"Kfc times prescribed displacement produced a nonfinite row sum.");
EXPECT_EQ(load[0U], 7.0);
EXPECT_EQ(prescribed[0U], 2.0);
EXPECT_EQ(prescribed[1U], 1.0);
EXPECT_EQ(kfc.Values(), original_values);
EXPECT_EQ(dofs.GetSparsePattern().row_offsets, original_pattern.row_offsets);
EXPECT_EQ(dofs.GetSparsePattern().column_indices,
original_pattern.column_indices);
}
+15
View File
@@ -17,6 +17,21 @@ TEST(Ascii, LowercasesOnlyAsciiUppercaseBytes) {
EXPECT_EQ(fesa::AsciiLower(non_ascii), non_ascii);
}
TEST(Ascii, UppercaseCanonicalizationPreservesEveryOtherByteAndSourceText) {
std::string source;
std::string expected;
for (unsigned int byte = 0U; byte < 256U; ++byte) {
source.push_back(static_cast<char>(byte));
expected.push_back(
static_cast<char>(byte >= 97U && byte <= 122U ? byte - 32U : byte));
}
const std::string original = source;
EXPECT_EQ(fesa::AsciiUppercase(source), expected);
EXPECT_EQ(source, original);
EXPECT_TRUE(fesa::AsciiUppercase("").empty());
EXPECT_EQ(fesa::AsciiUppercase("Shell-01"), "SHELL-01");
}
TEST(Ascii, ComparesNamesWithoutLocaleOrUnicodeFolding) {
EXPECT_TRUE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "bEaM-1"));
EXPECT_FALSE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "Beam-10"));
+8
View File
@@ -17,4 +17,12 @@ TEST(CoreDiagnostics, SourceIdentityPreservesRawIdentity) {
EXPECT_EQ(identity.instance_name, "Beam-Instance_A");
EXPECT_EQ(identity.source_label, 42);
EXPECT_EQ(identity.source_label_text, "00042");
EXPECT_TRUE(fesa::SourceIdentityEquals(identity, identity));
EXPECT_FALSE(fesa::SourceIdentityEquals(
identity, {"beam-instance_a", std::int64_t{42}, "00042"}));
EXPECT_FALSE(fesa::SourceIdentityEquals(
identity, {"Beam-Instance_A", std::int64_t{43}, "00042"}));
EXPECT_FALSE(fesa::SourceIdentityEquals(
identity, {"Beam-Instance_A", std::int64_t{42}, "42"}));
}
+61
View File
@@ -3,6 +3,7 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
@@ -19,8 +20,68 @@ fesa::Diagnostic ModelDiagnostic() {
"Beam length must be positive."};
}
struct CopyOnlyPayload {
explicit CopyOnlyPayload(int initial_value) : value{initial_value} {}
CopyOnlyPayload(const CopyOnlyPayload& other)
: value{other.value}, copies{other.copies + 1} {}
int value;
int copies = 0;
};
struct TransferPayload {
explicit TransferPayload(int initial_value) : value{initial_value} {}
TransferPayload(const TransferPayload& other)
: value{other.value}, copies{other.copies + 1}, moves{other.moves} {}
TransferPayload(TransferPayload&& other) noexcept
: value{other.value}, copies{other.copies}, moves{other.moves + 1} {
other.value = -1;
}
int value;
int copies = 0;
int moves = 0;
};
} // namespace
TEST(CoreDiagnostics, ResultOwnsCopyOnlyPayloadWithoutIntermediateCopy) {
CopyOnlyPayload source{23};
auto result = fesa::Result<CopyOnlyPayload>::Success(source);
ASSERT_TRUE(result.HasValue());
EXPECT_TRUE(result.GetStatus().IsOk());
EXPECT_EQ(result.Value().value, 23);
EXPECT_EQ(result.Value().copies, 2);
result.Value().value = 42;
EXPECT_EQ(source.value, 23);
EXPECT_EQ(source.copies, 0);
}
TEST(CoreDiagnostics, ResultTransfersOwnedPayloadDirectlyIntoValue) {
TransferPayload source{17};
const auto from_lvalue = fesa::Result<TransferPayload>::Success(source);
EXPECT_EQ(source.value, 17);
EXPECT_EQ(source.copies, 0);
EXPECT_EQ(source.moves, 0);
EXPECT_EQ(from_lvalue.Value().value, 17);
EXPECT_EQ(from_lvalue.Value().copies, 1);
EXPECT_EQ(from_lvalue.Value().moves, 1);
const auto from_temporary =
fesa::Result<TransferPayload>::Success(TransferPayload{29});
EXPECT_EQ(from_temporary.Value().value, 29);
EXPECT_EQ(from_temporary.Value().copies, 0);
EXPECT_EQ(from_temporary.Value().moves, 1);
auto owned = std::make_unique<int>(31);
const auto move_only =
fesa::Result<std::unique_ptr<int>>::Success(std::move(owned));
EXPECT_EQ(owned, nullptr);
ASSERT_NE(move_only.Value(), nullptr);
EXPECT_EQ(*move_only.Value(), 31);
}
TEST(CoreDiagnostics, ResultEnforcesValueErrorExclusivity) {
const fesa::Status ok = fesa::Status::Ok();
EXPECT_TRUE(ok.IsOk());
@@ -0,0 +1,413 @@
#include "fesa/elements/element_capabilities.h"
#include <gtest/gtest.h>
#include <array>
#include <cstring>
#include <limits>
#include <memory>
#include <type_traits>
#include <vector>
#include "fesa/elements/element_factory.h"
#include "fesa/elements/hex8_element.h"
#include "fesa/elements/nonlinear_mitc4_inertia.h"
#include "fesa/elements/thermal_shell.h"
namespace {
void Same(double a, double b) {
EXPECT_EQ(std::memcmp(&a, &b, sizeof(double)), 0);
}
template <std::size_t N>
void Same(const std::array<double, N>& a, const std::array<double, N>& b) {
for (std::size_t i = 0; i < N; ++i) Same(a[i], b[i]);
}
void Same(const fesa::Vector& a, const fesa::Vector& b) {
ASSERT_EQ(a.Size(), b.Size());
for (std::size_t i = 0; i < a.Size(); ++i) Same(a[i], b[i]);
}
void Same(const fesa::Matrix& a, const fesa::Matrix& b) {
ASSERT_EQ(a.Rows(), b.Rows());
ASSERT_EQ(a.Columns(), b.Columns());
for (std::size_t i = 0; i < a.Rows(); ++i)
for (std::size_t j = 0; j < a.Columns(); ++j) Same(a(i, j), b(i, j));
}
void Same(const fesa::Vector3& a, const fesa::Vector3& b) {
for (std::size_t i = 0; i < 3; ++i) Same(a[i], b[i]);
}
void Same(const fesa::Status& a, const fesa::Status& b) {
EXPECT_EQ(a.Category(), b.Category());
ASSERT_EQ(a.Diagnostics().size(), b.Diagnostics().size());
for (std::size_t i = 0; i < a.Diagnostics().size(); ++i) {
const auto& x = a.Diagnostics()[i];
const auto& y = b.Diagnostics()[i];
EXPECT_EQ(x.code, y.code);
EXPECT_EQ(x.location.file, y.location.file);
EXPECT_EQ(x.location.line, y.location.line);
EXPECT_EQ(x.keyword, y.keyword);
EXPECT_EQ(x.entity_identity, y.entity_identity);
EXPECT_EQ(x.message, y.message);
}
}
fesa::ElementDofLayout ShellLayout() {
return {{"shell", 9, "0009"}, {7, 2, 9, 4},
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz, fesa::DofComponent::kUrx,
fesa::DofComponent::kUry, fesa::DofComponent::kUrz}};
}
struct ShellInput {
std::array<fesa::Node, 4> nodes{{
{{"shell", 1, "1"}, {-1., -1., 0.}, {"cap.inp", 1}},
{{"shell", 2, "2"}, {1., -1., 0.}, {"cap.inp", 2}},
{{"shell", 3, "3"}, {1., 1., 0.}, {"cap.inp", 3}},
{{"shell", 4, "4"}, {-1., 1., 0.}, {"cap.inp", 4}}}};
std::array<fesa::ShellNodeInitialFrame, 4> frames{{
{0, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
{1, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
{2, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}},
{3, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}}}};
fesa::ShellSection section{"section", .2, 0, {"cap.inp", 20}};
fesa::LinearElasticMaterial material{"elastic", 120., .25, {"cap.inp", 21}};
std::array<const fesa::Node*, 4> Pointers() const {
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
}
};
fesa::NonlinearMitc4TrialDofs Trial() {
fesa::NonlinearMitc4TrialDofs trial{};
for (std::size_t i = 0; i < 4; ++i) {
const double n = static_cast<double>(i + 1);
trial.displacement[i] = {.001 * n, -.002 * n, .0005 * n};
trial.rotation_vector[i] = {.01 * n, -.005 * n, .002 * n};
}
return trial;
}
void Same(const fesa::NonlinearMitc4Recovery& a,
const fesa::NonlinearMitc4Recovery& b) {
Same(a.physical_strain_energy, b.physical_strain_energy);
for (std::size_t g = 0; g < 4; ++g) {
const auto& x = a.gauss_points[g];
const auto& y = b.gauss_points[g];
EXPECT_EQ(x.location, y.location);
for (std::size_t i = 0; i < 2; ++i)
Same(x.natural_coordinates[i], y.natural_coordinates[i]);
for (std::size_t i = 0; i < 8; ++i) {
Same(x.generalized_strain[i], y.generalized_strain[i]);
Same(x.section_resultant[i], y.section_resultant[i]);
}
Same(x.local_frame.e1, y.local_frame.e1);
Same(x.local_frame.e2, y.local_frame.e2);
Same(x.local_frame.e3, y.local_frame.e3);
for (std::size_t s = 0; s < 3; ++s)
for (std::size_t i = 0; i < x.stress[s].components.size(); ++i)
Same(x.stress[s].components[i], y.stress[s].components[i]);
}
}
TEST(ElementCapabilities, ShellTrialPreservesCompleteDirectionalResponseAndFailure) {
ShellInput input;
auto made = fesa::NonlinearMitc4Shell::Create(
input.Pointers(), input.frames, input.section, input.material);
ASSERT_TRUE(made.HasValue());
auto kernel = std::make_unique<fesa::NonlinearMitc4Shell>(std::move(made.Value()));
std::unique_ptr<fesa::ShellTrialElement> capability =
std::make_unique<fesa::Mitc4TrialAdapter>(ShellLayout(), *kernel);
const auto trial = Trial();
fesa::Vector direction{24};
for (std::size_t i = 0; i < 24; ++i) direction[i] = .003 * (i + 1);
const auto saved_direction = direction;
const auto direct = fesa::EvaluateInternalResidualAndTangent(*kernel, trial, &direction);
const auto via = capability->EvaluateTrial(trial, &direction);
ASSERT_TRUE(direct.HasValue());
ASSERT_TRUE(via.HasValue());
const auto& a = direct.Value();
const auto& b = via.Value();
Same(a.internal_residual, b.internal_residual);
Same(a.internal_tangent, b.internal_tangent);
Same(a.internal_force, b.internal_force);
Same(a.recovery, b.recovery);
Same(a.physical_strain_energy, b.physical_strain_energy);
Same(a.physical_force, b.physical_force);
Same(a.drilling_force, b.drilling_force);
Same(a.drilling_tangent, b.drilling_tangent);
Same(a.drilling_metric, b.drilling_metric);
Same(a.drilling_energy, b.drilling_energy);
Same(a.force_directional_second, b.force_directional_second);
Same(a.drilling_constraint, b.drilling_constraint);
Same(a.drilling_constraint_gradient, b.drilling_constraint_gradient);
for (std::size_t n = 0; n < 4; ++n) {
Same(a.drilling_constraint_hessian[n], b.drilling_constraint_hessian[n]);
Same(a.drilling_constraint_third_directional_gradient[n],
b.drilling_constraint_third_directional_gradient[n]);
}
Same(direction, saved_direction);
EXPECT_EQ(capability->DofLayout().node_indices, ShellLayout().node_indices);
EXPECT_EQ(capability->DofLayout().components_per_node, ShellLayout().components_per_node);
auto invalid = trial;
invalid.rotation_vector[2] = {std::numeric_limits<double>::quiet_NaN(), 0., 0.};
const auto failed_direct = fesa::EvaluateInternalResidualAndTangent(*kernel, invalid);
const auto failed_via = capability->EvaluateTrial(invalid);
ASSERT_FALSE(failed_direct.HasValue());
ASSERT_FALSE(failed_via.HasValue());
Same(failed_direct.GetStatus(), failed_via.GetStatus());
const auto again = capability->EvaluateTrial(trial, &direction);
ASSERT_TRUE(again.HasValue());
Same(again.Value().internal_force, a.internal_force);
}
TEST(ElementCapabilities, ShellExternalConstraintRetainsEveryVariation) {
ShellInput input;
const auto kernel = fesa::NonlinearMitc4Shell::Create(
input.Pointers(), input.frames, input.section, input.material);
ASSERT_TRUE(kernel.HasValue());
fesa::Mitc4TrialAdapter adapter{ShellLayout(), kernel.Value()};
const fesa::ShellTrialElement& capability = adapter;
const fesa::Vector3 phi{.12, -.23, .07}, rate{.31, .02, -.17};
const fesa::Vector3 force{1., -2., 3.}, force_rate{.2, .7, -.4}, acceleration{.1, -.3, .9};
const auto direct = kernel.Value().EvaluateExternalDrillingConstraint(
2, phi, rate, force, force_rate, acceleration);
const auto via = capability.EvaluateExternalDrillingConstraint(
2, phi, rate, force, force_rate, acceleration);
ASSERT_TRUE(direct.HasValue());
ASSERT_TRUE(via.HasValue());
const auto& a = direct.Value();
const auto& b = via.Value();
Same(a.value, b.value);
Same(a.gradient, b.gradient);
Same(a.hessian, b.hessian);
Same(a.directional_first, b.directional_first);
Same(a.directional_second, b.directional_second);
Same(a.gradient_directional_first, b.gradient_directional_first);
Same(a.gradient_directional_second, b.gradient_directional_second);
const auto failed_direct = kernel.Value().EvaluateExternalDrillingConstraint(
4, phi, rate, force, force_rate, acceleration);
const auto failed_via = capability.EvaluateExternalDrillingConstraint(
4, phi, rate, force, force_rate, acceleration);
ASSERT_FALSE(failed_direct.HasValue());
ASSERT_FALSE(failed_via.HasValue());
Same(failed_direct.GetStatus(), failed_via.GetStatus());
}
TEST(ElementCapabilities, ShellInertiaRetainsCompletePhysicalResponse) {
ShellInput input;
const auto kernel = fesa::NonlinearMitc4Inertia::Create(
input.Pointers(), input.frames, input.section, input.material, 2.);
ASSERT_TRUE(kernel.HasValue());
std::unique_ptr<fesa::ShellInertiaElement> capability =
std::make_unique<fesa::Mitc4InertiaAdapter>(ShellLayout(), kernel.Value());
const auto trial = Trial();
fesa::Vector v{24}, acceleration{24};
for (std::size_t i = 0; i < 24; ++i) {
v[i] = .013 * (i + 1);
acceleration[i] = -.007 * (i + 1);
}
const auto saved_v = v, saved_acceleration = acceleration;
const auto direct = kernel.Value().Evaluate(trial, v, acceleration);
const auto via = capability->Evaluate(trial, v, acceleration);
ASSERT_TRUE(direct.HasValue());
ASSERT_TRUE(via.HasValue());
const auto& a = direct.Value();
const auto& b = via.Value();
Same(a.mass, b.mass);
Same(a.position_tangent, b.position_tangent);
Same(a.velocity_tangent, b.velocity_tangent);
Same(a.force, b.force);
Same(a.convective_force, b.convective_force);
Same(a.kinetic_energy, b.kinetic_energy);
Same(a.momentum, b.momentum);
Same(a.angular_momentum, b.angular_momentum);
Same(a.momentum_rate, b.momentum_rate);
Same(a.angular_momentum_rate, b.angular_momentum_rate);
Same(a.momentum_rate_scale, b.momentum_rate_scale);
Same(a.angular_momentum_rate_scale, b.angular_momentum_rate_scale);
Same(v, saved_v);
Same(acceleration, saved_acceleration);
const auto failed_direct = kernel.Value().Evaluate(trial, fesa::Vector{23}, acceleration);
const auto failed_via = capability->Evaluate(trial, fesa::Vector{23}, acceleration);
ASSERT_FALSE(failed_direct.HasValue());
ASSERT_FALSE(failed_via.HasValue());
Same(failed_direct.GetStatus(), failed_via.GetStatus());
}
TEST(ElementCapabilities, ThermalPreservesStationLayoutIntervalsAndAllTraces) {
const std::array<fesa::Vector3, 4> x{{{0., 0., 0.}, {1., 0., 0.},
{1., 1., 0.}, {0., 1., 0.}}};
const auto kernel = fesa::ThermalShell::Create(
{"thermal", 19, "0019"}, {7, 2, 9, 4}, x, 2., 3., 4., .5);
ASSERT_TRUE(kernel.HasValue());
std::unique_ptr<fesa::ThermalElement> capability =
std::make_unique<fesa::Ds4ThermalAdapter>(kernel.Value());
EXPECT_EQ(capability->DofLayout().source_id.source_label_text, "0019");
EXPECT_EQ(capability->DofLayout().node_indices, kernel.Value().NodeIndices());
for (std::size_t i = 0; i < 5; ++i)
EXPECT_EQ(static_cast<int>(capability->DofLayout().components[i]), 11 + i);
EXPECT_EQ(&capability->Conductivity(), &kernel.Value().Conductivity());
EXPECT_EQ(&capability->Capacity(), &kernel.Value().Capacity());
EXPECT_EQ(&capability->ConductivityError(), &kernel.Value().ConductivityError());
EXPECT_EQ(&capability->CapacityError(), &kernel.Value().CapacityError());
std::array<double, 20> temperature{};
for (std::size_t i = 0; i < 20; ++i) temperature[i] = static_cast<double>(i * i);
const auto saved = temperature;
const auto direct = kernel.Value().Recover(temperature);
const auto via = capability->Recover(temperature);
ASSERT_TRUE(direct.HasValue());
ASSERT_TRUE(via.HasValue());
for (std::size_t i = 0; i < 24; ++i)
Same(direct.Value().global_flux[i], via.Value().global_flux[i]);
for (std::size_t p = 0; p < 4; ++p)
for (std::size_t i = 0; i < 3; ++i)
Same(direct.Value().frames[p][i], via.Value().frames[p][i]);
EXPECT_EQ(temperature, saved);
temperature[9] = std::numeric_limits<double>::infinity();
const auto failed_direct = kernel.Value().Recover(temperature);
const auto failed_via = capability->Recover(temperature);
ASSERT_FALSE(failed_direct.HasValue());
ASSERT_FALSE(failed_via.HasValue());
Same(failed_direct.GetStatus(), failed_via.GetStatus());
}
fesa::Result<fesa::Hex8Element> Solid() {
const fesa::Hex8Element::Coordinates x{{
{-1., -1., -1.}, {1., -1., -1.}, {1., 1., -1.}, {-1., 1., -1.},
{-1., -1., 1.}, {1., -1., 1.}, {1., 1., 1.}, {-1., 1., 1.}}};
return fesa::Hex8Element::Create(
3, {{"solid", 42, "0042"}, {7, 2, 9, 4, 8, 3, 10, 5},
{fesa::DofComponent::kUx, fesa::DofComponent::kUy,
fesa::DofComponent::kUz}},
x, fesa::LinearElasticMaterial{"elastic", 120., .25, {}}, 2.);
}
void Same(const fesa::Hex8Response& a, const fesa::Hex8Response& b) {
Same(a.internal_force, b.internal_force);
Same(a.material_tangent, b.material_tangent);
Same(a.geometric_tangent, b.geometric_tangent);
Same(a.tangent, b.tangent);
Same(a.strain_energy, b.strain_energy);
for (std::size_t i = 0; i < 8; ++i) {
const auto& x = a.points[i];
const auto& y = b.points[i];
EXPECT_EQ(x.element, y.element);
EXPECT_EQ(x.integration_point, y.integration_point);
Same(x.natural_coordinates, y.natural_coordinates);
Same(x.reference_coordinates, y.reference_coordinates);
Same(x.strain, y.strain);
Same(x.cauchy_stress, y.cauchy_stress);
}
}
TEST(ElementCapabilities, SolidRetainsLinearNonlinearAndPairedResponses) {
auto made = Solid();
ASSERT_TRUE(made.HasValue());
const auto& kernel = made.Value();
const fesa::Hex8TrialAdapter adapter{kernel};
const fesa::SolidTrialElement& capability = adapter;
const fesa::Element& base = capability;
EXPECT_EQ(&base.DofLayout(), &kernel.DofLayout());
fesa::Vector u{24}, low{24};
for (std::size_t i = 0; i < 24; ++i) {
u[i] = .001 * static_cast<double>(i + 1);
low[i] = 1e-20 * static_cast<double>(i + 1);
}
const auto saved_u = u, saved_low = low;
const auto linear = kernel.EvaluateLinear(u);
const auto via_linear = capability.EvaluateLinear(u);
const auto nonlinear = kernel.EvaluateNonlinear(u);
const auto via_nonlinear = capability.EvaluateNonlinear(u);
ASSERT_TRUE(linear.HasValue());
ASSERT_TRUE(via_linear.HasValue());
ASSERT_TRUE(nonlinear.HasValue());
ASSERT_TRUE(via_nonlinear.HasValue());
Same(linear.Value(), via_linear.Value());
Same(nonlinear.Value(), via_nonlinear.Value());
for (const bool nonlinear_path : {false, true}) {
const auto direct = nonlinear_path ? kernel.EvaluateNonlinearHighLow(u, low)
: kernel.EvaluateLinearHighLow(u, low);
const auto via = nonlinear_path ? capability.EvaluateNonlinearHighLow(u, low)
: capability.EvaluateLinearHighLow(u, low);
ASSERT_TRUE(direct.HasValue());
ASSERT_TRUE(via.HasValue());
Same(direct.Value().high, via.Value().high);
Same(direct.Value().internal_force_low, via.Value().internal_force_low);
Same(direct.Value().tangent_low, via.Value().tangent_low);
Same(direct.Value().strain_energy_low, via.Value().strain_energy_low);
}
const auto k = kernel.ComputeStiffness(), via_k = base.ComputeStiffness();
const auto m = kernel.ComputeMass(), via_m = capability.ComputeMass();
ASSERT_TRUE(k.HasValue());
ASSERT_TRUE(via_k.HasValue());
ASSERT_TRUE(m.HasValue());
ASSERT_TRUE(via_m.HasValue());
Same(k.Value().values, via_k.Value().values);
Same(m.Value().values, via_m.Value().values);
Same(kernel.ReferenceWeights(), capability.ReferenceWeights());
const auto recovered = base.Recover(u), direct_recovered = kernel.Recover(u);
ASSERT_TRUE(recovered.HasValue());
ASSERT_TRUE(direct_recovered.HasValue());
EXPECT_EQ(recovered.Value().source_id.source_label_text, "0042");
const auto& rows = std::get<fesa::SolidElementResultRows>(recovered.Value().payload);
const auto& direct_rows = std::get<fesa::SolidElementResultRows>(direct_recovered.Value().payload);
ASSERT_EQ(rows.rows.size(), direct_rows.rows.size());
for (std::size_t i = 0; i < rows.rows.size(); ++i) {
EXPECT_EQ(rows.rows[i].element, direct_rows.rows[i].element);
EXPECT_EQ(rows.rows[i].integration_point, direct_rows.rows[i].integration_point);
Same(rows.rows[i].cauchy_stress, direct_rows.rows[i].cauchy_stress);
Same(rows.rows[i].strain, direct_rows.rows[i].strain);
}
Same(u, saved_u);
Same(low, saved_low);
const auto failed = capability.EvaluateNonlinear(fesa::Vector{23});
const auto direct_failed = kernel.EvaluateNonlinear(fesa::Vector{23});
ASSERT_FALSE(failed.HasValue());
ASSERT_FALSE(direct_failed.HasValue());
Same(failed.GetStatus(), direct_failed.GetStatus());
const auto paired_failed = capability.EvaluateNonlinearHighLow(u, fesa::Vector{23});
const auto direct_paired_failed = kernel.EvaluateNonlinearHighLow(u, fesa::Vector{23});
ASSERT_FALSE(paired_failed.HasValue());
ASSERT_FALSE(direct_paired_failed.HasValue());
Same(paired_failed.GetStatus(), direct_paired_failed.GetStatus());
const auto again = capability.EvaluateNonlinear(u);
ASSERT_TRUE(again.HasValue());
Same(again.Value(), nonlinear.Value());
}
TEST(ElementCapabilities, BoundViewsSurviveOwnerVectorMovesAndRejectOtherFamilies) {
static_assert(std::has_virtual_destructor_v<fesa::ShellTrialElement>);
static_assert(std::has_virtual_destructor_v<fesa::SolidTrialElement>);
static_assert(std::has_virtual_destructor_v<fesa::ShellInertiaElement>);
static_assert(std::has_virtual_destructor_v<fesa::ThermalElement>);
static_assert(std::has_virtual_destructor_v<fesa::ShellReferenceElement>);
auto made = Solid();
ASSERT_TRUE(made.HasValue());
std::vector<std::unique_ptr<fesa::Element>> owners;
owners.push_back(std::make_unique<fesa::Hex8Element>(std::move(made.Value())));
const auto* original = owners[0].get();
const fesa::ElementFactory factory;
auto capability = factory.BindSolidTrial(*original);
ASSERT_NE(capability, nullptr);
EXPECT_EQ(factory.BindShellReference(*original), nullptr);
// Reallocation moves unique_ptr owners, never the kernels they own.
owners.reserve(32);
auto transferred = std::move(owners);
EXPECT_EQ(transferred[0].get(), original);
EXPECT_EQ(&capability->DofLayout(), &original->DofLayout());
const auto evaluated = capability->EvaluateLinear(fesa::Vector{24});
ASSERT_TRUE(evaluated.HasValue());
capability.reset(); // Destruction through the role must not delete its kernel.
const auto recovered = original->Recover(fesa::Vector{24});
ASSERT_TRUE(recovered.HasValue());
ShellInput input;
auto shell = fesa::Mitc4Shell::Create(
input.Pointers(), {{{0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}, {0., 0., 1.}}},
input.section, input.material);
ASSERT_TRUE(shell.HasValue());
EXPECT_EQ(factory.BindSolidTrial(shell.Value()), nullptr);
auto reference = factory.BindShellReference(shell.Value());
ASSERT_NE(reference, nullptr);
EXPECT_EQ(&reference->DofLayout(), &shell.Value().DofLayout());
const auto direct_k = shell.Value().Stiffness();
const auto via_k = reference->Stiffness();
ASSERT_TRUE(direct_k.HasValue());
ASSERT_TRUE(via_k.HasValue());
Same(direct_k.Value().stabilized_global24, via_k.Value().stabilized_global24);
}
} // namespace
+27 -10
View File
@@ -161,6 +161,28 @@ TEST(FiniteRotation, RejectsCoordinatesInsidePrincipalBranchMargin) {
"invalid-accepted-state");
}
TEST(FiniteRotation, SpatialMomentSolvePreservesKnownLoadAndVirtualWork) {
const Vector3 moment{2.0, -3.0, 4.0};
const Vector3 variation{0.3, -0.5, 0.7};
const double pi = std::acos(-1.0);
for (const double angle : {0.0, 1e-9, 0.7, pi - 1e-6}) {
const double a = angle == 0.0 ? 1.0 : std::sin(angle) / angle;
const double b = angle < 1e-8 ? angle / 2.0 :
(1.0 - std::cos(angle)) / angle;
// Independent z-axis J^T*m; the axial component is unchanged.
const Vector3 generalized{a * moment.X() + b * moment.Y(),
-b * moment.X() + a * moment.Y(), moment.Z()};
const auto solved = SpatialMomentSO3(Vector3{0, 0, angle}, generalized);
ASSERT_TRUE(solved.HasValue());
ExpectVectorNear(solved.Value(), moment, 1e-11);
const Vector3 spatial_variation{a * variation.X() - b * variation.Y(),
b * variation.X() + a * variation.Y(),
variation.Z()};
EXPECT_NEAR(generalized.Dot(variation),
solved.Value().Dot(spatial_variation), 1e-11);
}
}
TEST(FiniteRotation,
JacobiansAreFiniteInverseTransposeAndDerivativeConsistent) {
const double pi = std::acos(-1.0);
@@ -172,18 +194,13 @@ TEST(FiniteRotation,
for (const Vector3& phi : rotations) {
const Matrix left_jacobian = LeftJacobianSO3(phi);
const Matrix inverse_transpose = InverseTransposeLeftJacobianSO3(phi);
EXPECT_TRUE(IsFinite(left_jacobian));
EXPECT_TRUE(IsFinite(inverse_transpose));
const Matrix identity =
Transpose(left_jacobian).Multiply(inverse_transpose);
for (std::size_t row = 0U; row < kRotationDimension; ++row) {
for (std::size_t column = 0U; column < kRotationDimension; ++column) {
EXPECT_NEAR(identity(row, column), row == column ? 1.0 : 0.0,
1.0e-11);
}
}
const Vector3 load{0.7, -0.2, 0.4};
const auto spatial = SpatialMomentSO3(phi, load);
ASSERT_TRUE(spatial.HasValue());
ExpectVectorNear(Multiply(Transpose(left_jacobian), spatial.Value()),
load, 1e-11);
const Vector3 director = Multiply(ExpSkew(phi), Vector3{0.0, 0.0, 1.0});
const Vector3 positive =
+14 -30
View File
@@ -811,36 +811,6 @@ TEST(Hex8Element, FiniteInputsThatOverflowDerivedArithmeticFailClosed) {
std::string::npos);
}
TEST(Hex8Element, BackendOperandRejectionRetainsOriginalStiffnessDiagnostic) {
Coordinates coordinates{};
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i)
coordinates[a][i] = 0.01 * kSigns[a][i];
// Original native probe: finite moduli/volume, but mu*gradient overflows.
// The failed diagnostic candidate must not become a scalar success fallback.
const auto element = Make(coordinates, 1e308, -0.5);
ASSERT_FALSE(element.HasValue());
EXPECT_EQ(element.GetStatus().Diagnostics()[0].code, "invalid-hex8");
EXPECT_EQ(element.GetStatus().Diagnostics()[0].message,
"Hex8 reference stiffness is nonfinite.");
}
TEST(Hex8Element, TensorFailureRetainsPriorityOverEnergyAndTangentOverflow) {
Coordinates coordinates{};
for (std::size_t a = 0; a < 8; ++a)
for (std::size_t i = 0; i < 3; ++i)
coordinates[a][i] = 0.01 * kSigns[a][i];
const auto element = Make(coordinates, 1e200, -0.5);
ASSERT_TRUE(element.HasValue());
const auto displacement = Affine(coordinates,
Tensor{{{1e100, 0, 0}, {0, 1e100, 0}, {0, 0, 1e100}}});
const auto response = element.Value().EvaluateNonlinear(displacement);
ASSERT_FALSE(response.HasValue());
EXPECT_EQ(response.GetStatus().Diagnostics()[0].code, "invalid-hex8");
EXPECT_EQ(response.GetStatus().Diagnostics()[0].message,
"Hex8 trial tensor is nonfinite.");
}
// Independent Gauss-Legendre abscissae from polynomial root iteration. This
// test does not call the kernel's shapes, Jacobian, inverse or quadrature.
std::vector<std::array<double, 2>> LegendreRule(unsigned count) {
@@ -1211,3 +1181,17 @@ TEST(Hex8Element, MeanVolumeWarpedLowOnlyStateRetainsWeightedCubicEnergy) {
EXPECT_NE(actual_cubic.high, 0);
}
} // namespace
TEST(Hex8Element, RejectsNonfiniteCanonicalJacobianActionBeforeFiniteGradients) {
auto coordinates = Brick();
for (std::size_t a = 0; a < 8U; ++a) {
coordinates[a][0] = kSigns[a][0] * std::ldexp(1.0, -1025);
coordinates[a][1] = static_cast<double>(kSigns[a][1]);
coordinates[a][2] = static_cast<double>(kSigns[a][2]);
}
const auto rejected = Make(coordinates);
ASSERT_FALSE(rejected.HasValue());
ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U);
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].message,
"Reference Jacobian inverse is nonfinite.");
}
-933
View File
@@ -2,839 +2,16 @@
#include <gtest/gtest.h>
#include <array>
#include <atomic>
#include <cfenv>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <future>
#include <limits>
#include <random>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
#include "fesa/materials/isotropic_linear_elastic_material.h"
#include "../../../src/fesa/math/high_low_arithmetic.h"
#include "../../../src/fesa/elements/hex8_geometry.h"
#include "../../../src/fesa/elements/hex8_pair_arithmetic.h"
#include "../../../src/fesa/elements/hex8_pair_simd.h"
#include "../math/high_low_arithmetic_original.h"
namespace {
using fesa::math_internal::HighLow;
using fesa::math_internal::Subtract;
std::uint64_t PairBits(double value) {
std::uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
}
void ExpectOriginalProduct(HighLow left, HighLow right) {
const auto actual = fesa::hex8_internal::MultiplyPair(left, right);
const auto expected = fesa::high_low_original::Multiply(
{left.high, left.low}, {right.high, right.low});
EXPECT_EQ(PairBits(actual.high), PairBits(expected.high));
EXPECT_EQ(PairBits(actual.low), PairBits(expected.low));
const auto actual_sum = fesa::hex8_internal::AddPair(left, right);
const auto expected_sum = fesa::high_low_original::Add(
{left.high, left.low}, {right.high, right.low});
EXPECT_EQ(PairBits(actual_sum.high), PairBits(expected_sum.high));
EXPECT_EQ(PairBits(actual_sum.low), PairBits(expected_sum.low));
}
TEST(Hex8HighLow, ScalarCrossProductShortcutMatchesOriginalBits) {
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
const double minimum = std::numeric_limits<double>::denorm_min();
const double maximum = std::numeric_limits<double>::max();
const double infinity = std::numeric_limits<double>::infinity();
const double nan = std::numeric_limits<double>::quiet_NaN();
std::vector<HighLow> pairs{{}, {-0.0, 0.0}, {0.0, -0.0}, {-0.0, -0.0},
{1, -0.0}, {-1, -0.0}, {1, 1}, {0, minimum}, {infinity, 0},
{0, infinity}, {nan, 0}, {0, nan}, {maximum}, {-maximum},
{minimum}, {-minimum}, {std::numeric_limits<double>::min()},
{0.5}, {std::nextafter(0.5, 1.0)},
{1, std::ldexp(1.0, -53)},
{std::nextafter(1.0, 2.0), -std::ldexp(1.0, -53)}};
for (const auto left : pairs)
for (const auto right : pairs) ExpectOriginalProduct(left, right);
for (int exponent = -1074; exponent <= 1023; ++exponent) {
const double value = std::ldexp(1.0, exponent);
for (double sign : {-1.0, 1.0})
for (const auto right : pairs) {
ExpectOriginalProduct(HighLow{sign * value}, right);
ExpectOriginalProduct(right, HighLow{sign * value});
}
}
std::mt19937_64 random(0x48e81234U);
for (int sample = 0; sample < 12000; ++sample) {
double left = 0;
double right = 0;
const auto left_bits = random();
const auto right_bits = random();
std::memcpy(&left, &left_bits, sizeof(left));
std::memcpy(&right, &right_bits, sizeof(right));
const auto pair = fesa::math_internal::TwoSum(left, right);
ExpectOriginalProduct(pair, HighLow{});
ExpectOriginalProduct(HighLow{}, pair);
ExpectOriginalProduct(pair, HighLow{right});
ExpectOriginalProduct(HighLow{right}, pair);
ExpectOriginalProduct(pair, pair);
}
}
fesa::hex8_internal::PacketPair RawPacketPair(HighLow value) {
return {value.high, value.low};
}
fesa::hex8_internal::PairPacket FrozenMaterialPacket(
const fesa::hex8_internal::MaterialPacketInput& input) {
using Original = fesa::high_low_original::HighLow;
const auto original = [](fesa::hex8_internal::PacketPair value) {
return Original{value.high, value.low};
};
fesa::hex8_internal::PairPacket result;
for (std::size_t lane = 0; lane < 4; ++lane) {
const auto first = fesa::high_low_original::Multiply(
original(input.deviatoric_left.values[lane]),
original(input.deviatoric_right.values[lane]));
const auto second = fesa::high_low_original::Multiply(
original(input.shear_left.values[lane]), original(input.shear_right.values[lane]));
const auto third = fesa::high_low_original::Multiply(
original(input.tensor_left.values[lane]), original(input.gradient_dot.values[lane]));
const auto value = fesa::high_low_original::Add(
fesa::high_low_original::Add(first, second), third);
const auto accumulated = fesa::high_low_original::Add(
original(input.accumulated.values[lane]),
fesa::high_low_original::Multiply(value, original(input.weight)));
result.values[lane] = {accumulated.high, accumulated.low};
}
return result;
}
bool ExpectFrozenMaterialPacket(
const fesa::hex8_internal::MaterialPacketInput& input) {
const auto expected = FrozenMaterialPacket(input);
fesa::hex8_internal::PairPacket scalar, selected;
fesa::hex8_internal::EvaluateMaterialPacketScalar(input, &scalar);
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
evaluator(input, &selected);
for (std::size_t lane = 0; lane < 4; ++lane) {
SCOPED_TRACE(lane);
EXPECT_EQ(PairBits(scalar.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(scalar.values[lane].low), PairBits(expected.values[lane].low));
EXPECT_EQ(PairBits(selected.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(selected.values[lane].low), PairBits(expected.values[lane].low));
}
return evaluator != fesa::hex8_internal::EvaluateMaterialPacketScalar;
}
TEST(Hex8HighLow, FourColumnMaterialPacketMatchesFrozenScalarBits) {
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
using fesa::hex8_internal::MaterialPacketInput;
RecordProperty("private_packet_backend",
fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar ? "scalar" : "avx2");
std::size_t packet_checks = 0, avx2_evaluator_checks = 0;
const double tiny = std::numeric_limits<double>::denorm_min();
const double maximum = std::numeric_limits<double>::max();
const std::vector<HighLow> boundary{{}, {-0.0, 0}, {0, -0.0},
{1}, {-1}, {1, -0.0}, {1, std::ldexp(1.0, -54)},
{-1, std::ldexp(1.0, -54)}, {tiny}, {-tiny},
{std::numeric_limits<double>::min()}, {maximum}, {-maximum},
{1, tiny}, {1, -tiny}, {std::nextafter(1.0, 2.0)},
{1, 1}, {0, tiny}, {std::numeric_limits<double>::infinity(), 0},
{std::numeric_limits<double>::quiet_NaN(), 0},
{std::numeric_limits<double>::signaling_NaN(), 0},
{0, std::numeric_limits<double>::signaling_NaN()}};
for (std::size_t first = 0; first < boundary.size(); ++first)
for (std::size_t second = 0; second < boundary.size(); ++second) {
MaterialPacketInput input{};
input.weight = {0.125};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = RawPacketPair(boundary[first]);
input.deviatoric_right.values[lane] = RawPacketPair(boundary[second]);
input.shear_left.values[lane] = {2, std::ldexp(1.0, -54)};
input.shear_right.values[lane] = {-0.5, std::ldexp(1.0, -56)};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {-0.25};
input.accumulated.values[lane] = lane == 0 ? fesa::hex8_internal::PacketPair{0, 0}
: fesa::hex8_internal::PacketPair{0.125, 0};
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
for (int exponent = -1074; exponent <= 1023; ++exponent) {
MaterialPacketInput input{};
input.weight = {1};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {
std::ldexp(lane % 2U == 0 ? 1.0 : -1.0, exponent)};
input.deviatoric_right.values[lane] = {lane < 2 ? 0.5 : 2.0};
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
std::mt19937_64 random(0x4c0116U);
for (std::size_t sample = 0; sample < 2048; ++sample) {
MaterialPacketInput input{};
const auto pair = [&]() {
const double high = std::ldexp(
1.0 + static_cast<double>(random() % 256U) / 256.0,
static_cast<int>(random() % 201U) - 100);
const double signed_high = (random() & 1U) != 0 ? high : -high;
const double low = sample % 3U == 0 ? 0.0 : std::ldexp(high, -55);
return fesa::hex8_internal::PacketPair{
signed_high, (random() & 1U) != 0 ? low : -low};
};
input.weight = sample % 2U == 0
? fesa::hex8_internal::PacketPair{0.125, 0} : pair();
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = pair();
input.deviatoric_right.values[lane] = pair();
input.shear_left.values[lane] = pair();
input.shear_right.values[lane] = pair();
input.tensor_left.values[lane] = pair();
input.gradient_dot.values[lane] = pair();
input.accumulated.values[lane] = sample % 4U == 0
? fesa::hex8_internal::PacketPair{0, 0} : pair();
}
avx2_evaluator_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++packet_checks;
}
RecordProperty("packet_oracle_checks", static_cast<int>(packet_checks));
RecordProperty("avx2_evaluator_packet_checks",
static_cast<int>(avx2_evaluator_checks));
}
TEST(Hex8HighLow, PacketSpecialLaneDoesNotChangeOtherColumns) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.125};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -55)};
input.shear_left.values[lane] = {0.5};
input.shear_right.values[lane] = {-0.25};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {0.125};
}
const auto reference = FrozenMaterialPacket(input);
const std::vector<HighLow> special{{}, {-0.0, 0}, {1},
{std::numeric_limits<double>::max()},
{std::numeric_limits<double>::denorm_min()}, {1, 1},
{std::numeric_limits<double>::quiet_NaN(), 0}};
for (std::size_t lane = 0; lane < 4; ++lane)
for (const auto value : special) {
auto changed = input;
changed.deviatoric_left.values[lane] = RawPacketPair(value);
ExpectFrozenMaterialPacket(changed);
fesa::hex8_internal::PairPacket actual;
fesa::hex8_internal::SelectMaterialPacketEvaluator()(changed, &actual);
for (std::size_t other = 0; other < 4; ++other)
if (other != lane) {
EXPECT_EQ(PairBits(actual.values[other].high), PairBits(reference.values[other].high));
EXPECT_EQ(PairBits(actual.values[other].low), PairBits(reference.values[other].low));
}
}
}
TEST(Hex8HighLow, UniformDyadicPacketsExerciseEachLowZeroShape) {
std::size_t checks = 0, avx2_checks = 0;
for (int shape = 0; shape < 4; ++shape)
for (int low_weight = 0; low_weight < 2; ++low_weight)
for (int zero_accumulator = 0; zero_accumulator < 2; ++zero_accumulator) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.5, low_weight != 0 ? std::ldexp(1.0, -57) : 0.0};
for (std::size_t lane = 0; lane < 4; ++lane) {
const bool left_low = (shape & 1) != 0;
const bool right_low = (shape & 2) != 0;
// Every lane has the same normalized nonzero dyadic inputs. Component
// LBE sums stay above -200 and all Add intermediates are bounded by 4.
// Thus each Multiply low-shape is uniform and its component products
// bypass zero/unsupported-tail fallback. No mixed-zero Add fallback
// is possible; the initial zero accumulator is uniform across lanes.
input.deviatoric_left.values[lane] = {1, left_low ? std::ldexp(1.0, -54) : 0};
input.deviatoric_right.values[lane] = {2, right_low ? std::ldexp(1.0, -54) : 0};
input.shear_left.values[lane] = {0.5, left_low ? std::ldexp(1.0, -56) : 0};
input.shear_right.values[lane] = {0.25, right_low ? std::ldexp(1.0, -57) : 0};
input.tensor_left.values[lane] = {0.75, left_low ? std::ldexp(1.0, -56) : 0};
input.gradient_dot.values[lane] = {0.125, right_low ? std::ldexp(1.0, -58) : 0};
input.accumulated.values[lane] = zero_accumulator != 0
? fesa::hex8_internal::PacketPair{0, 0}
: fesa::hex8_internal::PacketPair{0.0625, 0};
}
avx2_checks += ExpectFrozenMaterialPacket(input) ? 1U : 0U;
++checks;
}
RecordProperty("uniform_low_shape_checks", static_cast<int>(checks));
RecordProperty("avx2_uniform_low_shape_checks", static_cast<int>(avx2_checks));
}
TEST(Hex8HighLow, OverflowLaneIsNeutralizedBeforeResidualFma) {
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
if (evaluator == fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "AVX2 masked environment is not selected; routing unverified.";
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
for (std::size_t bad = 0; bad < 4; ++bad) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.5};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {
lane == bad ? std::numeric_limits<double>::max() : 1};
input.deviatoric_right.values[lane] = {2};
input.shear_left.values[lane] = {0.5};
input.shear_right.values[lane] = {0.25};
input.tensor_left.values[lane] = {0.75};
input.gradient_dot.values[lane] = {0.125};
input.accumulated.values[lane] = {0.0625};
}
// All operands are finite/nonzero and max*2 has LBE sum 972, so Product
// reaches its vector multiply; only the post-product finite mask rejects it.
const auto expected = FrozenMaterialPacket(input);
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
fesa::hex8_internal::PairPacket output;
evaluator(input, &output);
const bool extra_invalid = (std::fetestexcept(FE_INVALID) & FE_INVALID) != 0;
EXPECT_EQ(std::fesetenv(&original), 0);
EXPECT_FALSE(extra_invalid);
for (std::size_t lane = 0; lane < 4; ++lane) {
EXPECT_EQ(PairBits(output.values[lane].high), PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(output.values[lane].low), PairBits(expected.values[lane].low));
}
}
RecordProperty("avx2_product_overflow_positions", 4);
}
#if defined(_M_X64) || defined(__SSE2__)
// Runs scalar and selected packet from identical FP state. The frozen original
// supplies raw-bit results; the unchanged baseline supplies FE/MXCSR status.
void ExpectPacketFlagsAndBits(
const fesa::hex8_internal::MaterialPacketInput& input, bool prior_status) {
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
const auto original_csr = _mm_getcsr();
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
struct Observation {
fesa::hex8_internal::PairPacket output;
int flags;
unsigned int status;
};
const auto observe = [&](fesa::hex8_internal::MaterialPacketEvaluator backend) {
EXPECT_EQ(std::fesetenv(&original), 0);
_mm_setcsr(original_csr & ~0x3fU);
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
if (prior_status) {
EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0);
_mm_setcsr(_mm_getcsr() | 0x02U); // Preserve prior denormal status too.
}
Observation value{};
backend(input, &value.output);
value.flags = std::fetestexcept(FE_ALL_EXCEPT);
value.status = _mm_getcsr() & 0x3fU;
return value;
};
const auto scalar = observe(fesa::hex8_internal::EvaluateMaterialPacketScalar);
const auto selected = observe(evaluator);
const auto expected = FrozenMaterialPacket(input);
// Restore both x87 and MXCSR before assertions, including NaN bit assertions.
EXPECT_EQ(std::fesetenv(&original), 0);
_mm_setcsr(original_csr);
EXPECT_EQ(selected.flags, scalar.flags);
EXPECT_EQ(selected.status, scalar.status);
for (std::size_t lane = 0; lane < 4; ++lane) {
SCOPED_TRACE(lane);
EXPECT_EQ(PairBits(selected.output.values[lane].high),
PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(selected.output.values[lane].low),
PairBits(expected.values[lane].low));
EXPECT_EQ(PairBits(scalar.output.values[lane].high),
PairBits(expected.values[lane].high));
EXPECT_EQ(PairBits(scalar.output.values[lane].low),
PairBits(expected.values[lane].low));
}
}
fesa::hex8_internal::MaterialPacketInput DyadicPacketFixture() {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {0.5, 0};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {1, 0};
input.deviatoric_right.values[lane] = {2, 0};
input.shear_left.values[lane] = {0.5, 0};
input.shear_right.values[lane] = {0.25, 0};
input.tensor_left.values[lane] = {0.75, 0};
input.gradient_dot.values[lane] = {0.125, 0};
}
return input;
}
TEST(Hex8HighLow, PacketVectorGuardsPreserveScalarFlagsAndBits) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "AVX2 masked environment is not selected.";
using fesa::hex8_internal::PacketPair;
const double maximum = std::numeric_limits<double>::max();
const double snan = std::numeric_limits<double>::signaling_NaN();
const double tiny = std::numeric_limits<double>::denorm_min();
const std::vector<PacketPair> invalid_left{
{-0.0, 0}, {0, -0.0}, {1, 1}, {snan, 0}, {0, snan}};
const std::vector<PacketPair> dangerous_right{
{maximum, maximum}, {snan, 0}, {0, snan}};
std::size_t checks = 0;
for (const auto left : invalid_left)
for (const auto right : dangerous_right)
for (std::size_t bad = 0; bad < 4; ++bad) {
auto input = DyadicPacketFixture();
input.deviatoric_left.values[bad] = left;
input.deviatoric_right.values[bad] = right;
// Later lanes must still contribute the unchanged bridge's RN status.
const auto later = (bad + 1) % 4;
input.deviatoric_left.values[later] = {1, std::ldexp(1.0, -54)};
input.deviatoric_right.values[later] = {tiny, 0};
ExpectPacketFlagsAndBits(input, false);
ExpectPacketFlagsAndBits(input, true);
checks += 2;
}
for (std::size_t lane = 0; lane < 4; ++lane) {
auto mixed = DyadicPacketFixture();
mixed.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
mixed.deviatoric_left.values[(lane + 1) % 4] = {maximum, maximum};
ExpectPacketFlagsAndBits(mixed, false);
ExpectPacketFlagsAndBits(mixed, true);
auto signed_low = DyadicPacketFixture();
signed_low.deviatoric_left.values[lane].low = -0.0;
signed_low.accumulated.values[lane] = {-0.0, 0};
ExpectPacketFlagsAndBits(signed_low, false);
checks += 3;
}
RecordProperty("avx2_guard_flags_and_bit_checks", static_cast<int>(checks));
}
TEST(Hex8HighLow, PacketTailScreenRetainsExactBoundaryAndFallback) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "AVX2 masked environment is not selected.";
const auto odd = [](int exponent) {
return std::nextafter(std::ldexp(1.0, exponent),
std::numeric_limits<double>::infinity());
};
const double tiny = std::numeric_limits<double>::denorm_min();
const std::array<std::array<double, 2>, 8> operands{{
{odd(-500), odd(-470)}, // screen true, exact LBE sum -1074
{odd(-500), odd(-471)}, // screen false, exact rejection -1075
{std::ldexp(1.0, -500), std::ldexp(1.0, -471)}, // false but supported
{tiny, odd(52)}, {tiny, odd(51)}, {3 * tiny, odd(52)},
{std::numeric_limits<double>::max(), 2}, {0, odd(52)}}};
std::size_t checks = 0;
for (const auto& pair : operands)
for (std::size_t special = 0; special < 4; ++special) {
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {1, 0};
for (std::size_t lane = 0; lane < 4; ++lane) {
const double sign = lane % 2 == 0 ? 1.0 : -1.0;
input.deviatoric_left.values[lane] =
{sign * (lane == special ? pair[0] : odd(-500)), 0};
input.deviatoric_right.values[lane] =
{lane == special ? pair[1] : odd(-470), 0};
}
ExpectPacketFlagsAndBits(input, false);
ExpectPacketFlagsAndBits(input, true);
checks += 2;
}
RecordProperty("avx2_tail_screen_flags_and_bit_checks", static_cast<int>(checks));
}
#endif
#if defined(_M_X64) || defined(__SSE2__)
fesa::hex8_internal::GeometryPacketOutput FrozenGeometryPacket(
const fesa::hex8_internal::GeometryPacketInput& input) {
const auto pair = [](fesa::hex8_internal::PacketPair value) {
return fesa::high_low_original::HighLow{value.high, value.low};
};
fesa::hex8_internal::GeometryPacketOutput output{};
for (std::size_t lane = 0; lane < 4; ++lane) {
fesa::high_low_original::HighLow dot;
for (std::size_t i = 0; i < 3; ++i)
dot = fesa::high_low_original::Add(dot,
fesa::high_low_original::Multiply(pair(input.gradient_a[i]),
pair(input.gradient_b[i].values[lane])));
fesa::high_low_original::HighLow geo;
for (std::size_t j = 0; j < 3; ++j)
for (std::size_t l = 0; l < 3; ++l)
geo = fesa::high_low_original::Add(geo,
fesa::high_low_original::Multiply(pair(input.gradient_stress[j][l]),
pair(input.gradient_b[l].values[lane])));
const auto weighted = fesa::high_low_original::Multiply(geo, pair(input.weight));
output.gradient_dot.values[lane] = {dot.high, dot.low};
output.weighted_geometric.values[lane] = {weighted.high, weighted.low};
}
return output;
}
fesa::hex8_internal::GeometryPacketInput GeometryDyadicFixture() {
fesa::hex8_internal::GeometryPacketInput input{};
input.weight = {0.125, 0};
for (std::size_t j = 0; j < 3; ++j) {
input.gradient_a[j] = {static_cast<double>(j + 1) * 0.25, 0};
for (std::size_t l = 0; l < 3; ++l) {
// Deliberately asymmetric first-products and distinct b/component mapping.
input.gradient_stress[j][l] =
{static_cast<double>(1 + 3 * j + l) * 0.125, 0};
for (std::size_t lane = 0; lane < 4; ++lane)
input.gradient_b[l].values[lane] =
{static_cast<double>(1 + 4 * l + lane) * 0.0625, 0};
}
}
return input;
}
// Original observation repeats geo*weight for three independent accumulators.
// Selected observation reuses the weighted pair but retains each checked Add.
void ExpectGeometryPacketBitsAndFlags(
const fesa::hex8_internal::GeometryPacketInput& input, bool prior_status) {
SCOPED_TRACE(prior_status);
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
const auto original_csr = _mm_getcsr();
struct Observation {
fesa::hex8_internal::GeometryPacketOutput output;
fesa::hex8_internal::PairPacket accumulators[3];
int flags;
unsigned int status;
};
const auto current_pair = [](fesa::hex8_internal::PacketPair value) {
return HighLow{value.high, value.low};
};
const auto observe = [&](bool original_order) {
EXPECT_EQ(std::fesetenv(&original), 0);
_mm_setcsr(original_csr & ~0x3fU);
EXPECT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0);
if (prior_status) {
EXPECT_EQ(std::feraiseexcept(FE_INEXACT), 0);
_mm_setcsr(_mm_getcsr() | 0x02U);
}
Observation value{};
if (!original_order)
fesa::hex8_internal::EvaluateGeometryPacketAvx2(input, &value.output);
for (std::size_t lane = 0; lane < 4; ++lane) {
HighLow geo;
if (original_order) {
HighLow dot;
for (std::size_t i = 0; i < 3; ++i)
dot = fesa::hex8_internal::AddPair(dot,
fesa::hex8_internal::MultiplyPair(current_pair(input.gradient_a[i]),
current_pair(input.gradient_b[i].values[lane])));
for (std::size_t j = 0; j < 3; ++j)
for (std::size_t l = 0; l < 3; ++l)
geo = fesa::hex8_internal::AddPair(geo,
fesa::hex8_internal::MultiplyPair(
current_pair(input.gradient_stress[j][l]),
current_pair(input.gradient_b[l].values[lane])));
value.output.gradient_dot.values[lane] = {dot.high, dot.low};
}
const HighLow accumulators[3]{{},
{static_cast<double>(lane + 1), std::ldexp(1.0, -54)}, {-1, -0.0}};
for (std::size_t component = 0; component < 3; ++component) {
const auto weighted = original_order
? fesa::hex8_internal::MultiplyPair(geo, current_pair(input.weight))
: current_pair(value.output.weighted_geometric.values[lane]);
if (original_order)
value.output.weighted_geometric.values[lane] =
{weighted.high, weighted.low};
const auto accumulated = fesa::hex8_internal::AddPair(
accumulators[component], weighted);
value.accumulators[component].values[lane] =
{accumulated.high, accumulated.low};
}
}
value.flags = std::fetestexcept(FE_ALL_EXCEPT);
value.status = _mm_getcsr() & 0x3fU;
return value;
};
const auto baseline = observe(true);
const auto selected = observe(false);
fesa::hex8_internal::GeometryPacketOutput scalar;
fesa::hex8_internal::EvaluateGeometryPacketScalar(input, &scalar);
const auto expected = FrozenGeometryPacket(input);
EXPECT_EQ(std::fesetenv(&original), 0);
_mm_setcsr(original_csr);
EXPECT_EQ(selected.flags, baseline.flags);
EXPECT_EQ(selected.status, baseline.status);
const auto same_bits = [](const fesa::hex8_internal::PairPacket& a,
const fesa::hex8_internal::PairPacket& b) {
for (std::size_t lane = 0; lane < 4; ++lane) {
EXPECT_EQ(PairBits(a.values[lane].high), PairBits(b.values[lane].high));
EXPECT_EQ(PairBits(a.values[lane].low), PairBits(b.values[lane].low));
}
};
same_bits(selected.output.gradient_dot, expected.gradient_dot);
same_bits(selected.output.weighted_geometric, expected.weighted_geometric);
same_bits(scalar.gradient_dot, expected.gradient_dot);
same_bits(scalar.weighted_geometric, expected.weighted_geometric);
for (std::size_t component = 0; component < 3; ++component)
same_bits(selected.accumulators[component], baseline.accumulators[component]);
}
TEST(Hex8HighLow, GeometryPacketPreservesAsymmetricLanesAndUniformShapes) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
RecordProperty("private_geometry_packet_backend", "avx2");
static_assert(std::is_trivial_v<fesa::hex8_internal::GeometryPacketInput>);
static_assert(std::is_trivial_v<fesa::hex8_internal::GeometryPacketOutput>);
std::size_t checks = 0;
for (int shape = 0; shape < 4; ++shape) {
auto input = GeometryDyadicFixture();
for (std::size_t j = 0; j < 3; ++j) {
if ((shape & 1) != 0) {
input.gradient_a[j].low = std::ldexp(input.gradient_a[j].high, -55);
for (std::size_t l = 0; l < 3; ++l)
input.gradient_stress[j][l].low =
std::ldexp(input.gradient_stress[j][l].high, -55);
}
if ((shape & 2) != 0)
for (std::size_t lane = 0; lane < 4; ++lane)
input.gradient_b[j].values[lane].low =
std::ldexp(input.gradient_b[j].values[lane].high, -55);
}
ExpectGeometryPacketBitsAndFlags(input, false);
ExpectGeometryPacketBitsAndFlags(input, true);
checks += 2;
}
RecordProperty("avx2_geometry_uniform_bit_flag_checks", static_cast<int>(checks));
}
TEST(Hex8HighLow, GeometryPacketRetainsSpecialLanesAndExactTailBoundary) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
using fesa::hex8_internal::PacketPair;
const double tiny = std::numeric_limits<double>::denorm_min();
const double maximum = std::numeric_limits<double>::max();
const std::vector<PacketPair> special{{0, 0}, {-0.0, 0}, {1, -0.0}, {1, 1},
{tiny, 0}, {maximum, maximum},
{std::numeric_limits<double>::signaling_NaN(), 0},
{0, std::numeric_limits<double>::signaling_NaN()}};
std::size_t checks = 0;
for (const auto pair : special)
for (std::size_t lane = 0; lane < 4; ++lane) {
auto input = GeometryDyadicFixture();
input.gradient_b[1].values[lane] = pair;
ExpectGeometryPacketBitsAndFlags(input, false);
ExpectGeometryPacketBitsAndFlags(input, true);
checks += 2;
}
const auto odd = [](int exponent) {
return std::nextafter(std::ldexp(1.0, exponent),
std::numeric_limits<double>::infinity());
};
for (int exponent : {-470, -471})
for (std::size_t lane = 0; lane < 4; ++lane) {
auto input = GeometryDyadicFixture();
input.gradient_stress[0][0] = {odd(-500), 0};
input.gradient_b[0].values[lane] = {odd(exponent), 0};
ExpectGeometryPacketBitsAndFlags(input, false);
checks++;
}
RecordProperty("avx2_geometry_special_tail_checks", static_cast<int>(checks));
}
TEST(Hex8HighLow, GeometryPacketPreservesOrderedOverflowAndPreparedInvalid) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
const double maximum = std::numeric_limits<double>::max();
std::size_t checks = 0;
for (bool overflow_order : {false, true}) {
SCOPED_TRACE(overflow_order);
fesa::hex8_internal::GeometryPacketInput input{};
input.weight = {0.5, 0};
for (std::size_t l = 0; l < 3; ++l)
for (std::size_t lane = 0; lane < 4; ++lane)
input.gradient_b[l].values[lane] = {1, 0};
input.gradient_stress[0][0] = {maximum, 0};
input.gradient_stress[0][1] = {overflow_order ? maximum : -maximum, 0};
input.gradient_stress[0][2] = {overflow_order ? -maximum : maximum, 0};
ExpectGeometryPacketBitsAndFlags(input, false);
ExpectGeometryPacketBitsAndFlags(input, true);
checks += 2;
}
auto input = GeometryDyadicFixture();
const auto invalid_first = fesa::high_low_original::Multiply(
{std::ldexp(1.0, -600), 0}, {std::ldexp(1.0, -500), 0});
input.gradient_stress[0][0] = {invalid_first.high, invalid_first.low};
for (std::size_t lane = 0; lane < 4; ++lane)
input.gradient_b[0].values[lane] = {std::ldexp(1.0, 600), 0};
ExpectGeometryPacketBitsAndFlags(input, false);
ExpectGeometryPacketBitsAndFlags(input, true);
checks += 2;
RecordProperty("avx2_geometry_ordered_invalid_checks", static_cast<int>(checks));
}
TEST(Hex8HighLow, GeometryPacketSumFallbackPreservesNearMaxAndSubnormalStatus) {
if (fesa::hex8_internal::SelectMaterialPacketEvaluator() ==
fesa::hex8_internal::EvaluateMaterialPacketScalar)
GTEST_SKIP() << "Geometry AVX2 masked environment is not selected.";
const double maximum = std::numeric_limits<double>::max();
const double normal = std::ldexp(1.0, -1022);
const double tiny = std::ldexp(1.0, -1074);
const double quarter = std::ldexp(1.0, 969);
std::size_t checks = 0;
for (double tail : {0.0, normal, tiny})
for (std::size_t bad = 0; bad < 4; ++bad) {
SCOPED_TRACE(tail);
SCOPED_TRACE(bad);
fesa::hex8_internal::GeometryPacketInput input{};
input.weight = {0.5, 0};
// Both pairs are normalized. Head sum is finite; middle contributes
// 2^970 and the joined sum overflows. Other lanes retain finite values.
input.gradient_stress[0][0] = {maximum, quarter};
input.gradient_stress[0][1] = {quarter, tail};
for (std::size_t lane = 0; lane < 4; ++lane) {
const double scale = lane == bad ? 1.0 : 0.5;
input.gradient_b[0].values[lane] = {scale, 0};
input.gradient_b[1].values[lane] = {scale, 0};
input.gradient_b[2].values[lane] = {1, 0};
}
ExpectGeometryPacketBitsAndFlags(input, false);
ExpectGeometryPacketBitsAndFlags(input, true);
checks += 2;
}
fesa::hex8_internal::GeometryPacketInput finite{};
finite.weight = {1, 0};
finite.gradient_stress[0][0] = {normal, 0};
finite.gradient_stress[0][1] = {normal + tiny, 0};
for (std::size_t lane = 0; lane < 4; ++lane) {
finite.gradient_b[0].values[lane] = {1, 0};
finite.gradient_b[1].values[lane] = {1, 0};
}
// TwoSum(N,N+d) has finite normal inputs and a subnormal error d.
ExpectGeometryPacketBitsAndFlags(finite, false);
ExpectGeometryPacketBitsAndFlags(finite, true);
checks += 2;
// Mixed prefix: finite subnormal-error lanes run before another lane's head
// overflow. The bridge must preserve the whole packet's status union.
for (std::size_t bad = 0; bad < 4; ++bad) {
SCOPED_TRACE(bad);
fesa::hex8_internal::GeometryPacketInput mixed{};
mixed.weight = {1, 0};
mixed.gradient_stress[0][2] = {normal, 0};
mixed.gradient_stress[1][2] = {normal + tiny, 0};
mixed.gradient_stress[2][0] = {maximum, 0};
mixed.gradient_stress[2][1] = {maximum, 0};
for (std::size_t lane = 0; lane < 4; ++lane) {
mixed.gradient_b[0].values[lane] = {lane == bad ? 1.0 : 0.0, 0};
mixed.gradient_b[1].values[lane] = {lane == bad ? 1.0 : 0.0, 0};
mixed.gradient_b[2].values[lane] = {lane == bad ? 0.0 : 1.0, 0};
}
ExpectGeometryPacketBitsAndFlags(mixed, false);
ExpectGeometryPacketBitsAndFlags(mixed, true);
checks += 2;
}
RecordProperty("avx2_geometry_sum_fallback_checks", static_cast<int>(checks));
}
#endif
TEST(Hex8HighLow, PacketBackendSelectionRecoversAfterEnvironmentRestoration) {
const auto initial = fesa::hex8_internal::SelectMaterialPacketEvaluator();
const int rounding = std::fegetround();
ASSERT_EQ(std::fesetround(FE_UPWARD), 0);
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
fesa::hex8_internal::EvaluateMaterialPacketScalar);
EXPECT_EQ(std::fesetround(rounding), 0);
#if defined(_M_X64) || defined(__SSE2__)
const auto control = _mm_getcsr();
_mm_setcsr(control | 0x8040U);
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(),
fesa::hex8_internal::EvaluateMaterialPacketScalar);
_mm_setcsr(control);
_mm_setcsr(control & ~0x80U);
const auto unmasked = fesa::hex8_internal::SelectMaterialPacketEvaluator();
_mm_setcsr(control);
EXPECT_EQ(unmasked, fesa::hex8_internal::EvaluateMaterialPacketScalar);
#endif
EXPECT_EQ(fesa::hex8_internal::SelectMaterialPacketEvaluator(), initial);
}
TEST(Hex8HighLow, PacketScalarBridgesPreserveOriginalRawBits) {
using fesa::hex8_internal::PacketPair;
using fesa::hex8_internal::PairPacket;
static_assert(std::is_trivial_v<PacketPair> &&
std::is_standard_layout_v<PacketPair>);
static_assert(std::is_trivial_v<PairPacket> &&
std::is_standard_layout_v<PairPacket>);
static_assert(std::is_trivial_v<fesa::hex8_internal::MaterialPacketInput>);
const double tiny = std::numeric_limits<double>::denorm_min();
const std::vector<PacketPair> pairs{{0, 0}, {-0.0, 0}, {0, -0.0},
{1, -0.0}, {1, std::ldexp(1.0, -54)}, {tiny, 0}, {1, tiny},
{std::numeric_limits<double>::max(), 0}, {1, 1},
{std::numeric_limits<double>::infinity(), 0},
{std::numeric_limits<double>::signaling_NaN(), 0}};
for (const auto left : pairs)
for (const auto right : pairs) {
PairPacket a{{left, left, left, left}};
PairPacket b{{right, right, right, right}};
PairPacket result;
const fesa::high_low_original::HighLow x{left.high, left.low};
const fesa::high_low_original::HighLow y{right.high, right.low};
const auto expect = [&](fesa::high_low_original::HighLow expected) {
for (std::size_t lane = 0; lane < 4; ++lane) {
EXPECT_EQ(PairBits(result.values[lane].high), PairBits(expected.high));
EXPECT_EQ(PairBits(result.values[lane].low), PairBits(expected.low));
}
};
fesa::hex8_internal::ScalarPacketAdd(a, b, false, &result);
expect(fesa::high_low_original::Add(x, y));
fesa::hex8_internal::ScalarPacketAdd(a, b, true, &result);
expect(fesa::high_low_original::Add(x, y));
fesa::hex8_internal::ScalarPacketMultiply(a, b, &result);
expect(fesa::high_low_original::Multiply(x, y));
const double high_left[4]{left.high, left.high, left.high, left.high};
const double high_right[4]{right.high, right.high, right.high, right.high};
fesa::hex8_internal::ScalarPacketTwoProduct(high_left, high_right, &result);
expect(fesa::high_low_original::TwoProduct(left.high, right.high));
}
}
TEST(Hex8HighLow, PacketCapabilityRejectsMissingIntegerIsaAndOsState) {
constexpr std::uint32_t leaf1 =
(1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
constexpr std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8);
EXPECT_TRUE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, 6, leaf7));
for (const auto missing : {1U << 3, 1U << 5, 1U << 8})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, 6, leaf7 & ~missing));
for (const auto missing : {1U << 12, 1U << 26, 1U << 27, 1U << 28})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1 & ~missing, 6, leaf7));
for (std::uint64_t state : {0ULL, 2ULL, 4ULL})
EXPECT_FALSE(fesa::hex8_internal::SupportsMaterialPacketInstructions(
leaf1, state, leaf7));
}
TEST(Hex8HighLow, PacketDoesNotClearPriorMaskedStickyFlags) {
std::fenv_t original;
ASSERT_EQ(std::fegetenv(&original), 0);
const auto evaluator = fesa::hex8_internal::SelectMaterialPacketEvaluator();
ASSERT_EQ(std::feraiseexcept(FE_INEXACT), 0);
fesa::hex8_internal::MaterialPacketInput input{};
input.weight = {1};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] = {1, std::ldexp(1.0, -54)};
input.deviatoric_right.values[lane] = {2, std::ldexp(1.0, -54)};
}
fesa::hex8_internal::PairPacket output;
evaluator(input, &output);
const bool preserved = (std::fetestexcept(FE_INEXACT) & FE_INEXACT) != 0;
EXPECT_EQ(std::fesetenv(&original), 0);
EXPECT_TRUE(preserved);
}
fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) {
fesa::ElementDofLayout layout;
layout.node_indices = {0, 1, 2, 3, 4, 5, 6, 7};
@@ -850,116 +27,6 @@ fesa::Hex8Element UnitBrick(double offset = 0, double nu = 0) {
return fesa::Hex8Element::Create(0, layout, coordinates, material, 2).Value();
}
void ExpectSamePairResponse(const fesa::Hex8HighLowResponse& actual,
const fesa::Hex8HighLowResponse& expected) {
EXPECT_EQ(PairBits(actual.high.strain_energy),
PairBits(expected.high.strain_energy));
EXPECT_EQ(PairBits(actual.strain_energy_low),
PairBits(expected.strain_energy_low));
ASSERT_EQ(actual.high.tangent.Rows(), 24U);
ASSERT_EQ(actual.tangent_low.Rows(), 24U);
for (std::size_t i = 0; i < 24; ++i) {
EXPECT_EQ(PairBits(actual.high.internal_force[i]),
PairBits(expected.high.internal_force[i]));
EXPECT_EQ(PairBits(actual.internal_force_low[i]),
PairBits(expected.internal_force_low[i]));
for (std::size_t j = 0; j < 24; ++j) {
EXPECT_EQ(PairBits(actual.high.tangent(i, j)),
PairBits(expected.high.tangent(i, j)));
EXPECT_EQ(PairBits(actual.tangent_low(i, j)),
PairBits(expected.tangent_low(i, j)));
}
}
for (std::size_t point = 0; point < 8; ++point) {
EXPECT_EQ(actual.high.points[point].integration_point,
expected.high.points[point].integration_point);
for (std::size_t i = 0; i < 6; ++i) {
EXPECT_EQ(PairBits(actual.high.points[point].cauchy_stress[i]),
PairBits(expected.high.points[point].cauchy_stress[i]));
EXPECT_EQ(PairBits(actual.high.points[point].strain[i]),
PairBits(expected.high.points[point].strain[i]));
}
}
}
TEST(Hex8HighLow, ConcurrentFirstLinearTrialsAcrossCopiesAndMoveKeepBits) {
const auto element = UnitBrick();
auto copied = element;
const auto moved = std::move(copied);
fesa::Vector high(24), low_a(24), low_b(24);
for (std::size_t i = 0; i < 24; ++i) high[i] = 1;
for (auto a : {1, 2, 5, 6}) {
low_a[3U * a] = std::ldexp(1.0, -60);
low_b[3U * a] = -std::ldexp(1.0, -59);
}
const auto oracle = UnitBrick();
const auto expected_a = oracle.EvaluateLinearHighLow(high, low_a);
const auto expected_b = oracle.EvaluateLinearHighLow(high, low_b);
ASSERT_TRUE(expected_a.HasValue());
ASSERT_TRUE(expected_b.HasValue());
std::promise<void> start;
const auto signal = start.get_future().share();
std::atomic<std::size_t> arrived{0U};
std::array<std::future<fesa::Result<fesa::Hex8HighLowResponse>>, 4> calls;
for (std::size_t i = 0; i < calls.size(); ++i)
calls[i] = std::async(std::launch::async, [&, i] {
arrived.fetch_add(1U, std::memory_order_release);
signal.wait();
return (i % 2U == 0U ? element : moved)
.EvaluateLinearHighLow(high, i < 2U ? low_a : low_b);
});
while (arrived.load(std::memory_order_acquire) != calls.size())
std::this_thread::yield();
start.set_value();
for (std::size_t i = 0; i < calls.size(); ++i) {
const auto response = calls[i].get();
ASSERT_TRUE(response.HasValue());
ExpectSamePairResponse(response.Value(),
i < 2U ? expected_a.Value() : expected_b.Value());
}
}
TEST(Hex8HighLow, FirstLinearPreparationRecoversAfterTrialAndEnvironmentFailure) {
ASSERT_TRUE(fesa::math_internal::ArithmeticEnvironmentSupported());
const auto element = UnitBrick();
fesa::Vector high(24), low(24);
EXPECT_FALSE(element.EvaluateLinearHighLow(fesa::Vector(23), low).HasValue());
high[0] = std::numeric_limits<double>::infinity();
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
high[0] = 0;
struct RestoreEnvironment {
int rounding = std::fegetround();
#if defined(_M_X64) || defined(__SSE2__)
unsigned csr = _mm_getcsr();
#endif
~RestoreEnvironment() {
std::fesetround(rounding);
#if defined(_M_X64) || defined(__SSE2__)
_mm_setcsr(csr);
#endif
}
} restore;
ASSERT_EQ(std::fesetround(FE_DOWNWARD), 0);
const auto bad_environment = element.EvaluateLinearHighLow(high, low);
ASSERT_FALSE(bad_environment.HasValue());
EXPECT_EQ(bad_environment.GetStatus().Diagnostics()[0].message,
"Hex8 high+low requires nearest rounding and gradual underflow.");
ASSERT_EQ(std::fesetround(FE_TONEAREST), 0);
#if defined(_M_X64) || defined(__SSE2__)
_mm_setcsr(restore.csr | 0x8040U);
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
_mm_setcsr(restore.csr);
#endif
const auto recovered = element.EvaluateLinearHighLow(high, low);
ASSERT_TRUE(recovered.HasValue());
high[3] = -2;
EXPECT_FALSE(element.EvaluateLinearHighLow(high, low).HasValue());
high[3] = 0;
const auto after_failure = element.EvaluateLinearHighLow(high, low);
ASSERT_TRUE(after_failure.HasValue());
ExpectSamePairResponse(after_failure.Value(), recovered.Value());
}
TEST(Hex8HighLow, LowOnlyAffineStrainSurvivesUniformHighTranslation) {
auto brick = UnitBrick();
fesa::Vector high(24), low(24);
+13
View File
@@ -1173,3 +1173,16 @@ TEST(Mitc4ShellPhysicalRecovery,
}
EXPECT_NEAR(recovery.strain_energy, 72.16, 1.0e-12);
}
TEST(Mitc4ShellKinematics, RetainsReciprocalDeterminantOverflowGateAtTinyScale) {
const std::array<fesa::Node, 4> nodes{
Node(0, {0.0, 0.0, 0.0}), Node(1, {2e-104, 0.0, 0.0}),
Node(2, {2e-104, 2e-104, 0.0}), Node(3, {0.0, 2e-104, 0.0})};
EXPECT_TRUE(fesa::Mitc4Shell::Create(
NodePointers(nodes), Directors(), Section(2e-99), Material()).HasValue());
const auto rejected = fesa::Mitc4Shell::Create(
NodePointers(nodes), Directors(), Section(2e-102), Material());
ASSERT_FALSE(rejected.HasValue());
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code,
"invalid-shell-jacobian");
}
@@ -494,25 +494,17 @@ TEST(DynamicRotation, InverseIdentityAndCurrentAnalyticNullAtFiniteRotation) {
for (const V3 phi : {V3{0.5, 0.0, 0.0}, V3{0.2, -0.3, 0.1}}) {
const auto rotation = fesa::ExpSkew(phi);
const auto jacobian = fesa::LeftJacobianSO3(phi);
const auto inverse_transpose = fesa::InverseTransposeLeftJacobianSO3(phi);
for (std::size_t column = 0; column < 3; ++column) {
// R^T e_j is a unit initial director; its current director is e_j.
const V3 initial{rotation(column, 0), rotation(column, 1),
rotation(column, 2)};
const auto value = fesa::EvaluateDynamicRotation(phi, initial, {}, {});
ASSERT_TRUE(value.HasValue());
const V3 inverse_column{inverse_transpose(column, 0),
inverse_transpose(column, 1),
inverse_transpose(column, 2)};
EXPECT_LE(
(value.Value().drill_null - inverse_column / inverse_column.Norm())
.Norm(),
1.0e-12);
const V3 identity_column =
Multiply(jacobian, value.Value().drill_null) * inverse_column.Norm();
for (std::size_t row = 0; row < 3; ++row) {
EXPECT_NEAR(identity_column[row], row == column ? 1.0 : 0.0, 1.0e-12);
}
const V3 mapped = Multiply(jacobian, value.Value().drill_null);
EXPECT_GT(mapped[column], 0.0);
EXPECT_NEAR(value.Value().drill_null.Norm(), 1.0, 1e-12);
for (std::size_t row = 0; row < 3; ++row)
if (row != column) EXPECT_NEAR(mapped[row], 0.0, 1e-12);
}
}
for (const Fixture fixture : {Fixture{}, Warped()}) {
+3
View File
@@ -14,6 +14,9 @@
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/elements/element.h"
namespace fesa {
class DofManagerTestAccess {
@@ -26,6 +26,9 @@
#include "io/hdf5/hdf5_primitives.h"
#include "io/hdf5/solid_hdf5_self_check.h"
#include "solid_output_test_fixture.h"
#include "fesa/analysis/solid_hht_dynamic_procedure.h"
#include "fesa/analysis/solid_static_procedure.h"
namespace {
using namespace fesa;
using namespace fesa::hdf5_internal;
+363 -234
View File
@@ -1,9 +1,9 @@
#include "math/dense_blas_internal.h"
#include "math/jet_linear_solve.h"
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <stdexcept>
@@ -11,129 +11,197 @@
namespace fesa::dense_blas_internal {
namespace {
TEST(DenseBlasInternal, PolicyQueryPreservesDefaultPositiveAndNestedTls) {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int global = mkl_get_max_threads();
const int domain = mkl_domain_get_max_threads(MKL_DOMAIN_BLAS);
const int dynamic = mkl_get_dynamic();
for (int repeat = 0; repeat < 2; ++repeat) {
const auto policy = ReadScopedLocalDensePolicy();
EXPECT_EQ(policy.blas_max_threads, 1);
EXPECT_EQ(policy.dynamic, dynamic);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
EXPECT_EQ(mkl_get_max_threads(), global);
EXPECT_EQ(mkl_domain_get_max_threads(MKL_DOMAIN_BLAS), domain);
EXPECT_EQ(mkl_get_dynamic(), dynamic);
}
static_cast<void>(mkl_set_num_threads_local(2));
EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 2);
EXPECT_EQ(mkl_set_num_threads_local(2), 2);
static_cast<void>(mkl_set_num_threads_local(0));
try {
const ScopedLocalDenseThreads outer{};
EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 1);
EXPECT_EQ(mkl_set_num_threads_local(1), 1);
throw std::runtime_error{"policy-query-unwind"};
} catch (const std::runtime_error&) {
}
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
EXPECT_EQ(mkl_get_dynamic(), dynamic);
// Forms RHS and residuals independently of the production dense adapter.
Matrix FactorRhs(const Matrix& a, const Matrix& x, const bool transpose,
const bool right) {
Matrix b{x.Rows(), x.Columns()};
for (std::size_t i = 0; i < b.Rows(); ++i)
for (std::size_t j = 0; j < b.Columns(); ++j)
for (std::size_t k = 0; k < a.Rows(); ++k)
b(i, j) += right ? x(i, k) * (transpose ? a(j, k) : a(k, j))
: (transpose ? a(k, i) : a(i, k)) * x(k, j);
return b;
}
TEST(DenseBlasInternal,
OuterProductUsesRectangularStorageAndPreservesRejection) {
const std::array<double, 2U> left{2.0, -3.0};
const std::array<double, 3U> right{4.0, -5.0, 0.25};
std::array<double, 6U> product{};
ASSERT_TRUE(OuterProduct(left.data(), left.size(), right.data(), right.size(),
product.data()));
const std::array<double, 6U> expected{8.0, -10.0, 0.5, -12.0, 15.0, -0.75};
EXPECT_EQ(product, expected);
const auto accepted = product;
const std::array<double, 2U> invalid{
1.0, std::numeric_limits<double>::quiet_NaN()};
EXPECT_FALSE(OuterProduct(invalid.data(), invalid.size(), right.data(),
right.size(), product.data()));
EXPECT_EQ(product, accepted);
EXPECT_TRUE(OuterProduct(nullptr, 0U, nullptr, 3U, nullptr));
EXPECT_TRUE(OuterProduct(nullptr, 2U, nullptr, 0U, nullptr));
EXPECT_THROW(OuterProduct(nullptr, 2U, right.data(), 3U, product.data()),
std::invalid_argument);
const auto too_large =
static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)()) + 1U;
EXPECT_THROW(OuterProduct(nullptr, too_large, nullptr, 1U, nullptr),
std::length_error);
const auto too_many_elements =
std::numeric_limits<std::size_t>::max() / sizeof(double) + 1U;
EXPECT_THROW(OuterProduct(nullptr, too_many_elements, nullptr, 1U, nullptr),
std::length_error);
void ExpectFactorSolution(const Matrix& a, const Matrix& x, const Matrix& b,
const Matrix& expected, const bool transpose,
const bool right) {
ASSERT_EQ(x.Rows(), expected.Rows());
ASSERT_EQ(x.Columns(), expected.Columns());
const auto residual = FactorRhs(a, x, transpose, right);
for (std::size_t i = 0; i < x.Rows(); ++i)
for (std::size_t j = 0; j < x.Columns(); ++j) {
EXPECT_DOUBLE_EQ(x(i, j), expected(i, j));
EXPECT_DOUBLE_EQ(residual(i, j), b(i, j));
}
}
TEST(DenseBlasInternal, LocalDenseScopeRestoresTlsAfterProductsAndUnwind) {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
const std::array<double, 2U> left{0.0, 2.0};
const std::array<double, 2U> right{3.0, 4.0};
std::array<double, 4U> product{};
product.fill(std::numeric_limits<double>::quiet_NaN());
{
const ScopedLocalDenseThreads scope{};
EXPECT_EQ(mkl_get_max_threads(), 1);
EXPECT_TRUE(
OuterProduct(left.data(), 2U, right.data(), 2U, product.data()));
EXPECT_DOUBLE_EQ(product[0U], 0.0);
EXPECT_DOUBLE_EQ(product[3U], 8.0);
}
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
try {
const ScopedLocalDenseThreads scope{};
OuterProduct(nullptr, 2U, right.data(), 2U, product.data());
} catch (const std::invalid_argument&) {
}
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
static_cast<void>(mkl_set_num_threads_local(3));
{
const ScopedLocalDenseThreads scope{};
EXPECT_EQ(mkl_get_max_threads(), 3);
}
EXPECT_EQ(mkl_get_max_threads(), 3);
TEST(DenseFactor, LuSolvesRectangularRhsOnBothSidesAndTransposes) {
Matrix a{3U, 3U};
const double values[]{0, 2, 1, 1, 1, 0, 2, 0, 1};
for (std::size_t i = 0; i < 9U; ++i) a.Data()[i] = values[i];
auto factor = DenseFactor::Lu(a);
ASSERT_TRUE(factor.HasValue());
EXPECT_DOUBLE_EQ(factor.Value().Determinant(), -4.0);
for (const bool right : {false, true})
for (const bool transpose : {false, true}) {
Matrix expected{right ? 2U : 3U, right ? 3U : 2U};
for (std::size_t i = 0; i < 6U; ++i)
expected.Data()[i] = static_cast<double>(i) - 2.0;
const auto b = FactorRhs(a, expected, transpose, right);
const auto solved = right ? factor.Value().SolveRight(b, transpose)
: factor.Value().Solve(b, transpose);
ASSERT_TRUE(solved.HasValue());
ExpectFactorSolution(a, solved.Value(), b, expected, transpose, right);
}
}
TEST(DenseBlasInternal, OuterProductLeavesFiniteInputOverflowForOwnerGate) {
const std::array<double, 1U> left{std::numeric_limits<double>::max()};
const std::array<double, 1U> right{2.0};
std::array<double, 1U> product{};
EXPECT_TRUE(OuterProduct(left.data(), 1U, right.data(), 1U, product.data()));
EXPECT_TRUE(std::isinf(product[0U]));
TEST(DenseFactor, CholeskySolvesAndWhiteningRetainsScaleAndOrientation) {
Matrix a{2U, 2U};
a(0, 0) = 4;
a(0, 1) = 2;
a(1, 0) = 2;
a(1, 1) = 5;
const auto factor = DenseFactor::Cholesky(a);
ASSERT_TRUE(factor.HasValue());
EXPECT_DOUBLE_EQ(factor.Value().Determinant(), 16.0);
const auto lower = factor.Value().Lower();
ASSERT_TRUE(lower.HasValue());
EXPECT_DOUBLE_EQ(lower.Value()(0, 0), 2.0);
EXPECT_DOUBLE_EQ(lower.Value()(0, 1), 0.0);
EXPECT_DOUBLE_EQ(lower.Value()(1, 0), 1.0);
EXPECT_DOUBLE_EQ(lower.Value()(1, 1), 2.0);
for (const bool right : {false, true})
for (const bool transpose : {false, true}) {
Matrix expected{right ? 3U : 2U, right ? 2U : 3U};
for (std::size_t i = 0; i < 6U; ++i)
expected.Data()[i] = static_cast<double>(i) - 2.0;
const auto b = FactorRhs(a, expected, transpose, right);
const auto solved = right ? factor.Value().SolveRight(b, transpose)
: factor.Value().Solve(b, transpose);
ASSERT_TRUE(solved.HasValue());
ExpectFactorSolution(a, solved.Value(), b, expected, transpose, right);
const auto triangular_rhs =
FactorRhs(lower.Value(), expected, transpose, right);
const auto triangular =
right ? factor.Value().SolveLowerRight(triangular_rhs, transpose)
: factor.Value().SolveLower(triangular_rhs, transpose);
ASSERT_TRUE(triangular.HasValue());
ExpectFactorSolution(lower.Value(), triangular.Value(), triangular_rhs,
expected, transpose, right);
}
// W = I * L^-T / sqrt(4); verify W^T (4*A) W = I without a procedure.
Matrix basis{3U, 2U};
basis(0, 0) = 0.5;
basis(1, 1) = 0.5;
const auto w = factor.Value().SolveLowerRight(basis, true);
ASSERT_TRUE(w.HasValue());
for (std::size_t i = 0; i < 2U; ++i)
for (std::size_t j = 0; j < 2U; ++j) {
double gram = 0;
for (std::size_t k = 0; k < 2U; ++k)
for (std::size_t l = 0; l < 2U; ++l)
gram += w.Value()(k, i) * (4 * a(k, l)) * w.Value()(l, j);
EXPECT_DOUBLE_EQ(gram, i == j ? 1.0 : 0.0);
}
}
TEST(DenseBlasInternal, TransposeRightProductUsesInnerAndRectangularStrides) {
const std::array<double, 6U> left{1, 2, 3, 4, 5, 6};
const std::array<double, 12U> right{7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18};
std::array<double, 8U> product;
ASSERT_TRUE(MultiplyTransposeRight(left.data(), 2U, right.data(), 4U, 3U,
product.data()));
const std::array<double, 8U> expected{50, 68, 86, 104, 122, 167, 212, 257};
EXPECT_EQ(product, expected);
EXPECT_TRUE(
MultiplyTransposeRight(nullptr, 2U, nullptr, 4U, 0U, product.data()));
for (const auto value : product) EXPECT_DOUBLE_EQ(value, 0.0);
auto nonfinite = right;
nonfinite[8U] = std::numeric_limits<double>::infinity();
product.fill(31.0);
EXPECT_FALSE(MultiplyTransposeRight(left.data(), 2U, nonfinite.data(), 4U, 3U,
product.data()));
for (const auto value : product) EXPECT_DOUBLE_EQ(value, 31.0);
TEST(DenseFactor, OwnsSnapshotAndFailedCandidatesPreserveReusableFactor) {
Matrix a{2U, 2U};
a(0, 0) = 2;
a(1, 1) = 4;
auto factor = DenseFactor::Lu(a);
ASSERT_TRUE(factor.HasValue());
a(0, 0) = 8;
const auto changed = DenseFactor::Lu(a);
ASSERT_TRUE(changed.HasValue());
Matrix rhs{2U, 1U, 8};
const auto old_solution = factor.Value().Solve(rhs);
ASSERT_TRUE(old_solution.HasValue());
EXPECT_DOUBLE_EQ(old_solution.Value()(0, 0), 4);
EXPECT_DOUBLE_EQ(changed.Value().Solve(rhs).Value()(0, 0), 1);
EXPECT_FALSE(DenseFactor::Lu(Matrix{2U, 2U}).HasValue());
EXPECT_FALSE(DenseFactor::Cholesky(Matrix{2U, 2U}).HasValue());
a(0, 0) = -1;
EXPECT_FALSE(DenseFactor::Cholesky(a).HasValue());
EXPECT_FALSE(DenseFactor::Lu(Matrix{2U, 3U}).HasValue());
a(0, 0) = (std::numeric_limits<double>::quiet_NaN)();
EXPECT_FALSE(DenseFactor::Lu(a).HasValue());
EXPECT_FALSE(DenseFactor::Cholesky(a).HasValue());
EXPECT_FALSE(factor.Value().Solve(Matrix{3U, 1U}).HasValue());
EXPECT_FALSE(factor.Value().SolveRight(Matrix{1U, 3U}).HasValue());
EXPECT_FALSE(factor.Value().SolveLower(rhs).HasValue());
rhs(0, 0) = (std::numeric_limits<double>::infinity)();
EXPECT_FALSE(factor.Value().Solve(rhs).HasValue());
EXPECT_DOUBLE_EQ(old_solution.Value()(0, 0), 4);
rhs(0, 0) = 8;
EXPECT_DOUBLE_EQ(factor.Value().Solve(rhs).Value()(0, 0), 4);
Matrix tiny{1U, 1U, (std::numeric_limits<double>::min)()};
const auto tiny_factor = DenseFactor::Lu(tiny);
ASSERT_TRUE(tiny_factor.HasValue());
EXPECT_FALSE(tiny_factor.Value()
.Solve(Matrix{1U, 1U, (std::numeric_limits<double>::max)()})
.HasValue());
EXPECT_DOUBLE_EQ(tiny_factor.Value().Solve(tiny).Value()(0, 0), 1);
}
TEST(DenseFactor, EmptyAndInfinityConditionBoundaryAreExplicit) {
for (const bool positive : {false, true}) {
const auto empty = positive ? DenseFactor::Cholesky(Matrix{0U, 0U})
: DenseFactor::Lu(Matrix{0U, 0U});
ASSERT_TRUE(empty.HasValue());
const auto left = empty.Value().Solve(Matrix{0U, 3U});
ASSERT_TRUE(left.HasValue());
EXPECT_EQ(left.Value().Columns(), 3U);
const auto right = empty.Value().SolveRight(Matrix{4U, 0U});
ASSERT_TRUE(right.HasValue());
EXPECT_EQ(right.Value().Rows(), 4U);
EXPECT_FALSE(empty.Value().ReciprocalConditionInfinity().HasValue());
for (const double diagonal : {0x1p-31, 0x1p-32, 0x1p-33}) {
Matrix a{2U, 2U};
a(0, 0) = 1;
a(1, 1) = diagonal;
const auto factor =
positive ? DenseFactor::Cholesky(a) : DenseFactor::Lu(a);
ASSERT_TRUE(factor.HasValue());
const auto rcond = factor.Value().ReciprocalConditionInfinity();
ASSERT_TRUE(rcond.HasValue());
EXPECT_DOUBLE_EQ(rcond.Value(), diagonal);
EXPECT_EQ(rcond.Value() >= 0x1p-32, diagonal >= 0x1p-32);
EXPECT_EQ(factor.Value().Solve(Matrix{2U, 0U}).Value().Columns(), 0U);
EXPECT_EQ(factor.Value().SolveRight(Matrix{0U, 2U}).Value().Rows(), 0U);
}
}
}
TEST(DenseFactor, GeneralDimensionAndNonDiagonalConditionUseFullCoefficient) {
Matrix a{8U, 8U};
a(0, 0) = 4;
for (std::size_t i = 1; i < 8U; ++i) {
a(i, i) = 5;
a(i, i - 1U) = 2;
a(i - 1U, i) = 2;
}
const auto factor = DenseFactor::Cholesky(a);
ASSERT_TRUE(factor.HasValue());
Matrix expected{8U, 3U};
for (std::size_t i = 0; i < 24U; ++i)
expected.Data()[i] = static_cast<double>(i) - 8;
const auto rhs = FactorRhs(a, expected, false, false);
const auto solved = factor.Value().Solve(rhs);
ASSERT_TRUE(solved.HasValue());
ExpectFactorSolution(a, solved.Value(), rhs, expected, false, false);
Matrix general{2U, 2U};
general(0, 0) = 2;
general(0, 1) = 1;
general(1, 1) = 1;
const auto lu = DenseFactor::Lu(general);
ASSERT_TRUE(lu.HasValue());
EXPECT_DOUBLE_EQ(lu.Value().ReciprocalConditionInfinity().Value(), 1.0 / 3.0);
EXPECT_FALSE(lu.Value().Lower().HasValue());
Matrix overflow{2U, 2U, (std::numeric_limits<double>::max)()};
overflow(1, 0) = -overflow(1, 0);
EXPECT_FALSE(DenseFactor::Lu(overflow).HasValue());
}
TEST(DenseBlasInternal, CDup003ConvertsZeroNormalAndOverflowLengths) {
@@ -162,138 +230,199 @@ TEST(DenseBlasInternal, CDup003CopiesZeroAndNonzeroContiguousValues) {
EXPECT_EQ(destination, source);
}
TEST(DenseBlasInternal, TransposeProductChecksShapeAndKnownRectangularAction) {
TEST(DenseBlasInternal, TransposeLeftMatchesIndependentRectangularProduct) {
Matrix left{3U, 2U};
Matrix right{3U, 1U};
Matrix right{3U, 4U};
for (std::size_t row = 0; row < 3U; ++row) {
left(row, 0U) = static_cast<double>(row + 1U);
left(row, 1U) = 2.0;
right(row, 0U) = static_cast<double>(row);
for (std::size_t column = 0; column < 2U; ++column)
left(row, column) = static_cast<double>(1U + row * 2U + column);
for (std::size_t column = 0; column < 4U; ++column)
right(row, column) = static_cast<double>(row + column) - 2.0;
}
const auto product = MultiplyTransposeLeft(left, right);
const Matrix product = MultiplyTransposeLeft(left, right);
ASSERT_EQ(product.Rows(), 2U);
ASSERT_EQ(product.Columns(), 1U);
EXPECT_DOUBLE_EQ(product(0U, 0U), 8.0);
EXPECT_DOUBLE_EQ(product(1U, 0U), 6.0);
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, Matrix{2U, 1U})),
ASSERT_EQ(product.Columns(), 4U);
for (std::size_t row = 0; row < 2U; ++row)
for (std::size_t column = 0; column < 4U; ++column) {
double expected = 0.0;
for (std::size_t inner = 0; inner < 3U; ++inner)
expected += left(inner, row) * right(inner, column);
EXPECT_DOUBLE_EQ(product(row, column), expected);
}
const auto gram = MultiplyTransposeLeft(left, left);
EXPECT_DOUBLE_EQ(gram(0U, 0U), 35.0);
EXPECT_DOUBLE_EQ(gram(0U, 1U), 44.0);
EXPECT_DOUBLE_EQ(gram(1U, 1U), 56.0);
}
TEST(DenseBlasInternal, TransposeLeftRetainsEmptyAndFailureContracts) {
const auto zeros = MultiplyTransposeLeft(Matrix{0U, 2U}, Matrix{0U, 3U});
ASSERT_EQ(zeros.Rows(), 2U);
ASSERT_EQ(zeros.Columns(), 3U);
EXPECT_DOUBLE_EQ(zeros(1U, 2U), 0.0);
const auto empty = MultiplyTransposeLeft(
Matrix{(std::numeric_limits<std::size_t>::max)(), 0U},
Matrix{(std::numeric_limits<std::size_t>::max)(), 0U});
EXPECT_EQ(empty.Rows(), 0U);
EXPECT_EQ(empty.Columns(), 0U);
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(Matrix{1U, 2U},
Matrix{2U, 1U})),
std::invalid_argument);
const auto empty = MultiplyTransposeLeft(Matrix{0U, 2U}, Matrix{0U, 3U});
EXPECT_EQ(empty.Rows(), 2U);
EXPECT_EQ(empty.Columns(), 3U);
EXPECT_DOUBLE_EQ(empty(1U, 2U), 0.0);
left(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, right)),
std::invalid_argument);
left(0U, 0U) = 1.0;
right(0U, 0U) = std::numeric_limits<double>::infinity();
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, right)),
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(
Matrix{1U, 0U}, Matrix{1U, 1U,
(std::numeric_limits<double>::quiet_NaN)()})),
std::invalid_argument);
}
TEST(DenseBlasInternal, DiagonalProductKeepsEntrywiseCancellationAndChecks) {
Vector diagonal{3U};
diagonal[0U] = 4;
diagonal[1U] = 2;
diagonal[2U] = -1;
Vector input{3U};
const double large = std::ldexp(1.0, 56);
input[0U] = 1;
input[1U] = -large;
input[2U] = 2 * large;
const auto product = MultiplyDiagonal(diagonal, input);
EXPECT_DOUBLE_EQ(product[0U] + (product[1U] - product[2U]), 4);
EXPECT_EQ(MultiplyDiagonal(Vector{0U}, Vector{0U}).Size(), 0U);
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, Vector{2U})),
std::invalid_argument);
input[0U] = std::numeric_limits<double>::infinity();
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, input)),
std::invalid_argument);
input[0U] = 1;
diagonal[0U] = std::numeric_limits<double>::quiet_NaN();
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, input)),
std::invalid_argument);
TEST(DenseBlasInternal, RawProductsPreserveDestinationOnNonfiniteInput) {
const double left[]{1.0, 2.0, -3.0, 4.0};
const double right[]{5.0, -6.0, 7.0, 8.0, -9.0, 10.0};
std::array<double, 6U> destination{};
ASSERT_TRUE(MultiplyTransposeRight(left, 2U, right, 3U, 2U,
destination.data()));
EXPECT_EQ(destination, (std::array<double, 6U>{-7.0, 23.0, 11.0,
-39.0, 11.0, 67.0}));
const double invalid[]{(std::numeric_limits<double>::infinity)(), 1.0};
destination.fill(42.0);
EXPECT_FALSE(MultiplyTransposeRight(invalid, 1U, right, 3U, 2U,
destination.data()));
EXPECT_FALSE(OuterProduct(left, 2U, invalid, 2U, destination.data()));
for (const double value : destination) EXPECT_DOUBLE_EQ(value, 42.0);
ASSERT_TRUE(OuterProduct(left, 2U, right, 3U, destination.data()));
EXPECT_EQ(destination, (std::array<double, 6U>{5.0, -6.0, 7.0,
10.0, -12.0, 14.0}));
}
TEST(DenseBlasInternal, CholeskyAndInverseMatchKnownFactorsAndKeepFailedOutput) {
Matrix matrix{2U, 2U};
matrix(0U, 0U) = 4;
matrix(0U, 1U) = 2;
matrix(1U, 0U) = 2;
matrix(1U, 1U) = 5;
Matrix lower{0U, 0U}, inverse{0U, 0U};
ASSERT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse));
EXPECT_DOUBLE_EQ(lower(0U, 0U), 2);
EXPECT_DOUBLE_EQ(lower(1U, 0U), 1);
EXPECT_DOUBLE_EQ(lower(1U, 1U), 2);
EXPECT_DOUBLE_EQ(lower(0U, 1U), 0);
EXPECT_DOUBLE_EQ(inverse(0U, 0U), .5);
EXPECT_DOUBLE_EQ(inverse(1U, 0U), -.25);
EXPECT_DOUBLE_EQ(inverse(1U, 1U), .5);
matrix(1U, 1U) = 0;
EXPECT_FALSE(CholeskyLowerAndInverse(matrix, lower, inverse));
EXPECT_DOUBLE_EQ(lower(1U, 1U), 2);
EXPECT_DOUBLE_EQ(inverse(1U, 1U), .5);
EXPECT_THROW(static_cast<void>(CholeskyLowerAndInverse(
Matrix{2U, 1U}, lower, inverse)), std::invalid_argument);
matrix(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
EXPECT_THROW(static_cast<void>(CholeskyLowerAndInverse(matrix, lower, inverse)),
TEST(DenseBlasInternal, RawProductZeroDimensionsAndOverflowAreExplicit) {
EXPECT_TRUE(OuterProduct(nullptr, 0U, nullptr, 3U, nullptr));
EXPECT_TRUE(MultiplyTransposeRight(nullptr, 2U, nullptr, 0U, 3U, nullptr));
std::array<double, 6U> zeros{1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
ASSERT_TRUE(MultiplyTransposeRight(nullptr, 2U, nullptr, 3U, 0U,
zeros.data()));
for (const double value : zeros) EXPECT_DOUBLE_EQ(value, 0.0);
EXPECT_THROW(MultiplyTransposeRight(nullptr, 1U, nullptr, 1U, 1U, nullptr),
std::invalid_argument);
const auto overflow =
static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)()) + 1U;
EXPECT_THROW(MultiplyTransposeRight(nullptr, overflow, nullptr, 0U, 0U,
nullptr), std::length_error);
double unchanged = 42.0;
EXPECT_THROW(CopyValues(&unchanged, overflow, &unchanged), std::length_error);
EXPECT_DOUBLE_EQ(unchanged, 42.0);
}
TEST(DenseBlasInternal,
CholeskyFailureReasonPreservesCandidatesAndResetsOnReuse) {
Matrix matrix{2U, 2U};
matrix(0U, 0U) = 4.0;
matrix(1U, 1U) = 9.0;
Matrix lower{0U, 0U};
Matrix inverse{0U, 0U};
auto reason = CholeskyFailure::kInverse;
ASSERT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason));
EXPECT_EQ(reason, CholeskyFailure::kNone);
matrix(1U, 1U) = -1.0;
EXPECT_FALSE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason));
EXPECT_EQ(reason, CholeskyFailure::kPivot);
EXPECT_DOUBLE_EQ(lower(1U, 1U), 3.0);
EXPECT_DOUBLE_EQ(inverse(1U, 1U), 1.0 / 3.0);
matrix(1U, 1U) = 9.0;
EXPECT_TRUE(CholeskyLowerAndInverse(matrix, lower, inverse, &reason));
EXPECT_EQ(reason, CholeskyFailure::kNone);
reason = CholeskyFailure::kInverse;
EXPECT_THROW(CholeskyLowerAndInverse(Matrix{2U, 1U}, lower, inverse, &reason),
std::invalid_argument);
EXPECT_EQ(reason, CholeskyFailure::kNone);
}
TEST(DenseBlasInternal, SingleEquationScopeRestoresTlsAndRespectsOverrides) {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
{
const ScopedSingleEquationThreads scope{1U};
EXPECT_EQ(mkl_get_max_threads(), 1);
}
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
{
const ScopedSingleEquationThreads scope{2U};
EXPECT_EQ(mkl_get_max_threads(), default_threads);
}
try {
const ScopedSingleEquationThreads scope{1U};
throw std::runtime_error{"scope failure"};
} catch (const std::runtime_error&) {
}
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
static_cast<void>(mkl_set_num_threads_local(3));
{
const ScopedSingleEquationThreads scope{1U};
EXPECT_EQ(mkl_get_max_threads(), 3);
}
EXPECT_EQ(mkl_get_max_threads(), 3);
TEST(DenseBlasInternal, ContiguousPrimitivesCheckEmptyAndOverflowBeforeUse) {
EXPECT_DOUBLE_EQ(DotValues(nullptr, 0U, nullptr), 0.0);
EXPECT_DOUBLE_EQ(NormValues(nullptr, 0U), 0.0);
EXPECT_NO_THROW(ScaleValues(2.0, nullptr, 0U));
EXPECT_NO_THROW(AxpyValues(2.0, nullptr, 0U, nullptr));
const auto overflow =
static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)()) + 1U;
double unchanged = 42.0;
EXPECT_THROW(DotValues(nullptr, overflow, nullptr), std::length_error);
EXPECT_THROW(NormValues(nullptr, overflow), std::length_error);
EXPECT_THROW(ScaleValues(2.0, &unchanged, overflow), std::length_error);
EXPECT_THROW(AxpyValues(2.0, nullptr, overflow, &unchanged), std::length_error);
EXPECT_DOUBLE_EQ(unchanged, 42.0);
}
} // namespace
} // namespace fesa::dense_blas_internal
namespace fesa::math_internal {
namespace {
// Taylor coefficients of t^i s^j through total degree three. This independent
// polynomial oracle deliberately uses coefficients, not derivative channels.
struct Polynomial {
std::array<std::array<double, 4>, 4> c{};
};
Polynomial operator-(Polynomial a, const Polynomial& b) {
for (std::size_t i = 0; i <= 3; ++i)
for (std::size_t j = 0; i + j <= 3; ++j) a.c[i][j] -= b.c[i][j];
return a;
}
Polynomial operator*(const Polynomial& a, const Polynomial& b) {
Polynomial result;
for (std::size_t i = 0; i <= 3; ++i)
for (std::size_t j = 0; i + j <= 3; ++j)
for (std::size_t k = 0; k <= i; ++k)
for (std::size_t l = 0; l <= j; ++l)
result.c[i][j] += a.c[k][l] * b.c[i-k][j-l];
return result;
}
Polynomial PolynomialConstant(double value) {
Polynomial p;
p.c[0][0] = value;
return p;
}
double PolynomialRead(const Polynomial& p, std::size_t degree,
std::size_t channel) {
return p.c[channel][degree-channel];
}
void PolynomialWrite(Polynomial& p, std::size_t degree, std::size_t channel,
double value) {
p.c[channel][degree-channel] = value;
}
TEST(JetLinearSolve, RecoversMixedCubicCoefficientsWithOnePrimalFactor) {
std::array<std::array<Polynomial, 3>, 3> a{};
const double values[]{0, 2, 1, 1, 1, 0, 2, 0, 1};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) {
a[i][j] = PolynomialConstant(values[3*i+j]);
for (std::size_t degree = 1; degree <= 3; ++degree)
for (std::size_t c = 0; c <= degree; ++c)
a[i][j].c[c][degree-c] = (static_cast<double>(i+2*j+c)-2) / 16;
}
std::array<Polynomial, 3> expected{}, rhs{};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t degree = 0; degree <= 3; ++degree)
for (std::size_t c = 0; c <= degree; ++c)
expected[i].c[c][degree-c] = (static_cast<double>(i+degree+2*c)-3) / 8;
// Form the full product independently using exact dyadic long-double sums.
for (std::size_t row = 0; row < 3; ++row)
for (std::size_t i = 0; i <= 3; ++i)
for (std::size_t j = 0; i+j <= 3; ++j) {
long double sum = 0;
for (std::size_t column = 0; column < 3; ++column)
for (std::size_t k = 0; k <= i; ++k)
for (std::size_t l = 0; l <= j; ++l)
sum += static_cast<long double>(a[row][column].c[k][l]) *
expected[column].c[i-k][j-l];
rhs[row].c[i][j] = static_cast<double>(sum);
}
Matrix primal{3, 3};
for (std::size_t i = 0; i < 3; ++i)
for (std::size_t j = 0; j < 3; ++j) primal(i,j) = a[i][j].c[0][0];
const auto factor = dense_blas_internal::DenseFactor::Lu(primal);
ASSERT_TRUE(factor.HasValue());
std::array<Polynomial, 3> solved{};
ASSERT_TRUE(SolveJetAction(a, rhs, factor.Value(), {1,2,3,4},
PolynomialConstant, PolynomialRead,
PolynomialWrite, solved));
for (std::size_t row = 0; row < 3; ++row)
for (std::size_t i = 0; i <= 3; ++i)
for (std::size_t j = 0; i+j <= 3; ++j)
EXPECT_NEAR(solved[row].c[i][j], expected[row].c[i][j], 2e-14);
}
TEST(JetLinearSolve, FailedChannelSolvePreservesCandidateAndInputs) {
std::array<std::array<Polynomial, 3>, 3> a{};
Matrix primal{3, 3};
for (std::size_t i = 0; i < 3; ++i) {
primal(i,i) = 1;
a[i][i] = PolynomialConstant(1);
}
const auto factor = dense_blas_internal::DenseFactor::Lu(primal);
ASSERT_TRUE(factor.HasValue());
std::array<Polynomial, 3> rhs{}, solved{};
rhs[1].c[1][0] = std::numeric_limits<double>::infinity();
solved[0].c[0][0] = 17;
EXPECT_FALSE(SolveJetAction(a, rhs, factor.Value(), {1,2,3,4},
PolynomialConstant, PolynomialRead,
PolynomialWrite, solved));
EXPECT_EQ(solved[0].c[0][0], 17);
EXPECT_EQ(a[1][1].c[0][0], 1);
EXPECT_EQ(rhs[1].c[1][0], std::numeric_limits<double>::infinity());
}
} // namespace
} // namespace fesa::math_internal
+72 -105
View File
@@ -1,15 +1,10 @@
#include "../../../src/fesa/math/high_low_arithmetic.h"
#include "elements/hex8_pair_simd.h"
#include <gtest/gtest.h>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <limits>
#include <random>
#include <vector>
#include "high_low_arithmetic_original.h"
namespace {
using namespace fesa::math_internal;
@@ -131,109 +126,81 @@ TEST(HighLowArithmetic, RejectsChangedRoundingAndFlushToZeroEnvironment) {
EXPECT_FALSE(ArithmeticEnvironmentSupported());
#endif
}
} // namespace
std::uint64_t Bits(const double value) {
std::uint64_t result = 0;
std::memcpy(&result, &value, sizeof(result));
return result;
}
double FromBits(const std::uint64_t bits) {
double result = 0;
std::memcpy(&result, &bits, sizeof(result));
return result;
}
TEST(HighLowArithmetic, FiniteClassificationMatchesBinary64Boundaries) {
const auto check = [](const double value) {
const bool expected = std::isfinite(value);
EXPECT_EQ(IsFinite(HighLow{value, 0.0}), expected);
EXPECT_EQ(IsFinite(HighLow{1.0, value}), expected);
};
for (std::uint64_t exponent = 0; exponent < 2048U; ++exponent) {
for (const auto fraction : {0ULL, 1ULL, 1ULL << 51U,
(1ULL << 52U) - 1ULL}) {
for (const auto sign : {0ULL, 1ULL << 63U})
check(FromBits(sign | (exponent << 52U) | fraction));
}
}
std::mt19937_64 random{0x5800f17U};
for (int sample = 0; sample < 10000; ++sample) check(FromBits(random()));
}
void ExpectSamePair(const HighLow actual,
const fesa::high_low_original::HighLow expected) {
EXPECT_EQ(Bits(actual.high), Bits(expected.high));
EXPECT_EQ(Bits(actual.low), Bits(expected.low));
}
TEST(HighLowArithmetic, BitExponentMatchesFrozenOracleAcrossEveryExponent) {
ASSERT_TRUE(ArithmeticEnvironmentSupported());
// Every normal exponent, every possible trailing significand-bit count,
// both signs; then every subnormal single-bit and mixed-bit boundary.
for (std::uint64_t exponent = 1; exponent < 2047; ++exponent) {
for (unsigned trailing = 0; trailing <= 52U; ++trailing) {
const auto fraction = trailing == 52U ? 0ULL : 1ULL << trailing;
for (const auto sign : {0ULL, 1ULL << 63U}) {
const double value = FromBits(sign | (exponent << 52U) | fraction);
ASSERT_EQ(LowestBitExponent(value),
fesa::high_low_original::LowestBitExponent(value));
namespace {
TEST(HighLowArithmetic, PacketDispatchRetainsComponentsAndInstructionGates) {
using namespace fesa::hex8_internal;
const std::uint32_t leaf1 = (1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
const std::uint32_t leaf7 = (1U << 3) | (1U << 5) | (1U << 8);
EXPECT_TRUE(SupportsMaterialPacketInstructions(leaf1, 6, leaf7));
for (const auto bit : {12,26,27,28})
EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1 & ~(1U << bit),6,leaf7));
for (const auto bit : {3,5,8})
EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,6,leaf7 & ~(1U << bit)));
EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,2,leaf7));
EXPECT_FALSE(SupportsMaterialPacketInstructions(leaf1,4,leaf7));
const auto selected = SelectMaterialPacketEvaluator();
std::size_t finite_pairs = 0, retained_low_pairs = 0, invalid_pairs = 0;
RecordProperty("selected_backend", selected == EvaluateMaterialPacketAvx2 ? "avx2" : "scalar");
const auto verify = [selected, &finite_pairs, &retained_low_pairs, &invalid_pairs](
const MaterialPacketInput& input) {
PairPacket expected{}, actual{};
EvaluateMaterialPacketScalar(input, &expected);
selected(input, &actual);
for (std::size_t lane = 0; lane < 4; ++lane) {
if (std::isnan(expected.values[lane].high)) {
++invalid_pairs;
EXPECT_TRUE(std::isnan(actual.values[lane].high));
} else {
++finite_pairs;
if (expected.values[lane].low != 0) ++retained_low_pairs;
EXPECT_DOUBLE_EQ(actual.values[lane].high, expected.values[lane].high);
EXPECT_DOUBLE_EQ(actual.values[lane].low, expected.values[lane].low);
EXPECT_EQ(std::signbit(actual.values[lane].low),
std::signbit(expected.values[lane].low));
}
}
}
for (unsigned bit = 0; bit < 52U; ++bit) {
for (const auto significand : {1ULL << bit, (1ULL << 52U) - (1ULL << bit)}) {
const double value = FromBits(significand);
EXPECT_EQ(LowestBitExponent(value),
fesa::high_low_original::LowestBitExponent(value));
}
}
}
TEST(HighLowArithmetic, OptimizedArithmeticMatchesFrozenOriginalBits) {
ASSERT_TRUE(ArithmeticEnvironmentSupported());
std::vector<double> values{0.0, -0.0,
std::numeric_limits<double>::denorm_min(),
-std::numeric_limits<double>::denorm_min(),
std::numeric_limits<double>::min(),
std::nextafter(std::numeric_limits<double>::min(), 0.0),
std::numeric_limits<double>::max(),
-std::numeric_limits<double>::max(), 1.0, -1.0,
std::nextafter(1.0, 2.0), std::ldexp(1.0, -53),
std::ldexp(1.0, -54), std::numeric_limits<double>::infinity(),
-std::numeric_limits<double>::infinity(),
std::numeric_limits<double>::quiet_NaN()};
std::mt19937_64 random{0x58a0fU};
for (int sample = 0; sample < 4000; ++sample) {
values.push_back(FromBits(random()));
}
for (std::size_t index = 0; index < values.size(); ++index) {
const double high = values[index];
const double low = values[(index * 13U + 7U) % values.size()];
const fesa::high_low_original::HighLow original{high, low};
EXPECT_EQ(IsNormalized({high, low}),
fesa::high_low_original::IsNormalized(original));
// Normalize a raw pair with the frozen operation, never the new predicate.
const auto a = fesa::high_low_original::TwoSum(high, low);
const auto b = fesa::high_low_original::TwoSum(
values[(index + 1U) % values.size()],
values[(index * 3U + 2U) % values.size()]);
ExpectSamePair(Add({a.high, a.low}, {b.high, b.low}),
fesa::high_low_original::Add(a, b));
ExpectSamePair(Multiply({a.high, a.low}, {b.high, b.low}),
fesa::high_low_original::Multiply(a, b));
ExpectSamePair(Divide({a.high, a.low}, {b.high, b.low}),
fesa::high_low_original::Divide(a, b));
ExpectSamePair(TwoProduct(high, low),
fesa::high_low_original::TwoProduct(high, low));
}
for (double high : {1.0, std::nextafter(1.0, 2.0), -1.0,
std::numeric_limits<double>::max()}) {
for (double low : {0.0, -0.0, std::ldexp(1.0, -53),
-std::ldexp(1.0, -53)}) {
EXPECT_EQ(IsNormalized({high, low}),
fesa::high_low_original::IsNormalized({high, low}));
};
for (const bool full_low : {false,true})
for (const int exponent : {-970,-100,0,100,400}) {
MaterialPacketInput input{};
input.weight = {0.5, 0};
for (std::size_t lane = 0; lane < 4; ++lane) {
input.deviatoric_left.values[lane] =
{std::ldexp(1.0, exponent), full_low ? std::ldexp(1.0,exponent-54) : 0};
input.deviatoric_right.values[lane] = {1.25, std::ldexp(1.0,-60)};
input.shear_left.values[lane] = {0, 0};
input.shear_right.values[lane] = {1,0};
input.tensor_left.values[lane] = {-1, std::ldexp(1.0,-55)};
input.gradient_dot.values[lane] = {std::ldexp(1.0,exponent),0};
input.accumulated.values[lane] = {std::ldexp(1.0,exponent),0};
}
verify(input);
// Mixed-low packets explicitly exercise the scalar bridge.
input.deviatoric_left.values[1].low = 0;
verify(input);
input.shear_left.values[3] = {-0.0,0}; // raw signed-zero is invalid by contract
input.deviatoric_left.values[0] = {std::numeric_limits<double>::max(),0};
input.deviatoric_right.values[0] = {2,0};
input.deviatoric_left.values[1] = {1,1};
input.deviatoric_left.values[2] = {std::numeric_limits<double>::denorm_min(),0};
input.deviatoric_right.values[2] = {0.5,0};
verify(input);
}
EXPECT_GT(finite_pairs, 0U);
EXPECT_GT(retained_low_pairs, 0U);
EXPECT_GT(invalid_pairs, 0U);
RecordProperty("finite_pairs", static_cast<int>(finite_pairs));
RecordProperty("retained_low_pairs", static_cast<int>(retained_low_pairs));
RecordProperty("invalid_pairs", static_cast<int>(invalid_pairs));
// Selection is thread-environment dependent rather than cached after first use.
#if defined(_M_X64) || defined(__SSE2__)
const unsigned saved = _mm_getcsr();
_mm_setcsr(saved | 0x8000U);
EXPECT_EQ(SelectMaterialPacketEvaluator(), EvaluateMaterialPacketScalar);
_mm_setcsr(saved);
EXPECT_EQ(SelectMaterialPacketEvaluator(), selected);
#endif
}
} // namespace
+55 -16
View File
@@ -2,6 +2,7 @@
#include <gtest/gtest.h>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
@@ -109,22 +110,60 @@ TEST(DenseMath, RowMajorMatrixMatchesKnownGemvGemm) {
std::invalid_argument);
}
TEST(DenseMath, MatrixProductsRejectNonfiniteOperandsAndPreserveValidReuse) {
Matrix left{2U, 2U, 1.0};
Matrix right{2U, 1U, 2.0};
Vector rhs{2U, 2.0};
rhs[1U] = std::numeric_limits<double>::infinity();
EXPECT_THROW(static_cast<void>(left.Multiply(rhs)), std::invalid_argument);
rhs[1U] = 2.0;
left(0U, 1U) = std::numeric_limits<double>::quiet_NaN();
EXPECT_THROW(static_cast<void>(left.Multiply(rhs)), std::invalid_argument);
EXPECT_THROW(static_cast<void>(left.Multiply(right)), std::invalid_argument);
left(0U, 1U) = 1.0;
right(1U, 0U) = std::numeric_limits<double>::infinity();
EXPECT_THROW(static_cast<void>(left.Multiply(right)), std::invalid_argument);
right(1U, 0U) = 2.0;
EXPECT_DOUBLE_EQ(left.Multiply(rhs)[0U], 4.0);
EXPECT_DOUBLE_EQ(left.Multiply(right)(1U, 0U), 4.0);
TEST(DenseMath, MatrixContiguousScaleAndRobustNorm) {
Matrix matrix{2U, 3U};
const double entries[]{1.0, -2.0, 3.0, -4.0, 5.0, -6.0};
for (std::size_t i = 0; i < 6U; ++i) matrix.Data()[i] = entries[i];
const Matrix& immutable = matrix;
EXPECT_EQ(immutable.Data() + 3, &immutable(1U, 0U));
EXPECT_NEAR(matrix.Norm(), std::sqrt(91.0), 2.0e-15);
matrix.Scale(-2.0);
for (std::size_t i = 0; i < 6U; ++i)
EXPECT_DOUBLE_EQ(matrix.Data()[i], -2.0 * entries[i]);
EXPECT_DOUBLE_EQ(immutable(1U, 2U), 12.0);
Matrix huge{1U, 2U, 1.0e308};
EXPECT_NEAR(huge.Norm() / 1.0e308, std::sqrt(2.0), 4.0e-16);
Matrix tiny{2U, 1U, 1.0e-300};
EXPECT_NEAR(tiny.Norm() / 1.0e-300, std::sqrt(2.0), 4.0e-16);
Matrix empty{0U, (std::numeric_limits<std::size_t>::max)()};
EXPECT_DOUBLE_EQ(empty.Norm(), 0.0);
EXPECT_NO_THROW(empty.Scale(3.0));
}
TEST(DenseMath, MatrixProductKeepsShapeAndFiniteValidationOrder) {
const double nan = (std::numeric_limits<double>::quiet_NaN)();
Matrix matrix{1U, 2U, nan};
try {
static_cast<void>(matrix.Multiply(Vector{1U}));
FAIL() << "Shape mismatch was accepted";
} catch (const std::invalid_argument& failure) {
EXPECT_STREQ(failure.what(),
"Matrix-vector multiplication has incompatible dimensions.");
}
EXPECT_THROW(static_cast<void>(matrix.Multiply(Vector{2U})),
std::invalid_argument);
EXPECT_THROW(static_cast<void>(matrix.Multiply(Matrix{2U, 0U})),
std::invalid_argument);
EXPECT_THROW(static_cast<void>(Matrix{0U, 2U}.Multiply(Vector{2U, nan})),
std::invalid_argument);
const Matrix finite{2U, 2U, 3.0};
const auto square = finite.Multiply(finite);
EXPECT_DOUBLE_EQ(square(0U, 0U), 18.0);
EXPECT_DOUBLE_EQ(finite(0U, 0U), 3.0);
}
TEST(DenseMath, MatrixSelfAssignmentPreservesShapeAndStorage) {
Matrix matrix{2U, 1U, 7.0};
const Matrix* same = &matrix;
matrix = *same;
EXPECT_EQ(matrix.Rows(), 2U);
EXPECT_EQ(matrix.Columns(), 1U);
EXPECT_DOUBLE_EQ(matrix(1U, 0U), 7.0);
Matrix* mutable_same = &matrix;
matrix = std::move(*mutable_same);
EXPECT_EQ(matrix.Rows(), 2U);
EXPECT_DOUBLE_EQ(matrix(1U, 0U), 7.0);
}
} // namespace
-102
View File
@@ -3,8 +3,6 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <future>
#include <limits>
#include <type_traits>
#include <utility>
@@ -133,104 +131,4 @@ TEST(SparseAssembly, PreservesExpectedStructuralZeros) {
4);
}
TEST(SparseAssembly, MultipliesRectangularCopyMoveAndConcurrentConstCalls) {
auto assembled = SparseMatrix::FromCoo(
3U, 5U, {{0U, 4U, 3.0, 0U, 0U}, {2U, 1U, -2.0, 1U, 0U}},
{{0U, 1U, 1U, 2U}, {4U, 1U}});
ASSERT_TRUE(assembled.HasValue());
const auto original = assembled.Value();
auto copied = original;
auto moved = std::move(copied);
Vector rhs{5U, 2.0};
const auto evaluate = [&rhs](const SparseMatrix& matrix) {
for (int repeat = 0; repeat < 20; ++repeat) {
const auto product = matrix.Multiply(rhs);
if (product.Size() != 3U || product[0U] != 6.0 ||
product[1U] != 0.0 || product[2U] != -4.0) return false;
}
return true;
};
auto first = std::async(std::launch::async, [&] { return evaluate(moved); });
auto second = std::async(std::launch::async, [&] { return evaluate(moved); });
EXPECT_TRUE(first.get());
EXPECT_TRUE(second.get());
EXPECT_TRUE(evaluate(original));
auto replacement = SparseMatrix::FromCoo(
1U, 1U, {{0U, 0U, 7.0, 0U, 0U}}, {{0U, 1U}, {0U}});
ASSERT_TRUE(replacement.HasValue());
moved = replacement.Value();
EXPECT_DOUBLE_EQ(moved.Multiply(Vector{1U, 3.0})[0U], 21.0);
EXPECT_TRUE(evaluate(original));
}
TEST(SparseAssembly, PreservesZeroShapesAndEmptyRows) {
auto no_rows = SparseMatrix::FromCoo(0U, 3U, {}, {{0U}, {}});
auto no_columns = SparseMatrix::FromCoo(2U, 0U, {}, {{0U, 0U, 0U}, {}});
auto no_entries = SparseMatrix::FromCoo(2U, 3U, {}, {{0U, 0U, 0U}, {}});
ASSERT_TRUE(no_rows.HasValue());
ASSERT_TRUE(no_columns.HasValue());
ASSERT_TRUE(no_entries.HasValue());
EXPECT_EQ(no_rows.Value().Multiply(Vector{3U}).Size(), 0U);
const auto empty_columns = no_columns.Value().Multiply(Vector{0U});
const auto empty_entries = no_entries.Value().Multiply(Vector{3U});
ASSERT_EQ(empty_columns.Size(), 2U);
ASSERT_EQ(empty_entries.Size(), 2U);
for (std::size_t row = 0; row < 2U; ++row) {
EXPECT_DOUBLE_EQ(empty_columns[row], 0.0);
EXPECT_DOUBLE_EQ(empty_entries[row], 0.0);
}
}
TEST(SparseAssembly, RejectsNonfiniteRhsAndRecoversAfterFiniteOverflow) {
auto matrix = SparseMatrix::FromCoo(
2U, 2U, {{0U, 0U, 2.0, 0U, 0U}}, {{0U, 1U, 2U}, {0U, 1U}});
ASSERT_TRUE(matrix.HasValue());
Vector rhs{2U, (std::numeric_limits<double>::infinity)()};
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(rhs)),
std::invalid_argument);
rhs[1U] = 4.0;
rhs[0U] = (std::numeric_limits<double>::max)();
EXPECT_TRUE(std::isinf(matrix.Value().Multiply(rhs)[0U]));
rhs[0U] = (std::numeric_limits<double>::quiet_NaN)();
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(rhs)),
std::invalid_argument);
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(Vector{1U})),
std::invalid_argument);
rhs[0U] = 3.0;
rhs[1U] = 4.0;
const auto finite = matrix.Value().Multiply(rhs);
EXPECT_DOUBLE_EQ(finite[0U], 6.0);
EXPECT_DOUBLE_EQ(finite[1U], 0.0);
}
TEST(SparseAssembly, MatchesIndependentLongDoubleResidualOnUnevenRows) {
const std::vector<double> entries{1.25, -0.375, 4.5, -3.0, 0.125, 2.0};
const SparsePattern pattern{{0U, 3U, 3U, 5U, 6U}, {0U, 2U, 4U, 1U, 3U, 2U}};
std::vector<CooContribution> contributions;
for (std::size_t row = 0; row < 4U; ++row) {
for (auto position = pattern.row_offsets[row];
position < pattern.row_offsets[row + 1U]; ++position) {
contributions.push_back(
{row, pattern.column_indices[position], entries[position], row,
position});
}
}
auto matrix = SparseMatrix::FromCoo(4U, 5U, contributions, pattern);
ASSERT_TRUE(matrix.HasValue());
Vector rhs{5U};
for (std::size_t column = 0; column < 5U; ++column) {
rhs[column] = 0.3 + static_cast<double>(column) * 0.7;
}
const auto product = matrix.Value().Multiply(rhs);
for (std::size_t row = 0; row < 4U; ++row) {
long double expected = 0;
for (auto position = pattern.row_offsets[row];
position < pattern.row_offsets[row + 1U]; ++position) {
expected += static_cast<long double>(entries[position]) *
rhs[pattern.column_indices[position]];
}
EXPECT_NEAR(product[row], static_cast<double>(expected), 2.0e-14);
}
}
} // namespace
+14
View File
@@ -112,5 +112,19 @@ TEST(Vector3, RejectsZeroAndNonfiniteNormalization) {
EXPECT_FALSE(Vector3(0.0, not_a_number, 0.0).Normalized().has_value());
}
TEST(Vector3, RightHandedFrameMatchesIndependentCoordinateTransform) {
const Vector3 x_axis{0.0, 1.0, 0.0};
const Vector3 y_axis{0.0, 0.0, 1.0};
const Vector3 z_axis = x_axis.Cross(y_axis);
const Vector3 global{2.0, -3.0, 4.0};
EXPECT_DOUBLE_EQ(x_axis.Dot(global), -3.0);
EXPECT_DOUBLE_EQ(y_axis.Dot(global), 4.0);
EXPECT_DOUBLE_EQ(z_axis.Dot(global), 2.0);
const Vector3 recovered = x_axis * -3.0 + y_axis * 4.0 + z_axis * 2.0;
EXPECT_TRUE(recovered == global);
EXPECT_DOUBLE_EQ(x_axis.Dot(y_axis.Cross(z_axis)), 1.0);
}
} // namespace
} // namespace fesa
+27
View File
@@ -3,6 +3,7 @@
#include <gtest/gtest.h>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <utility>
@@ -83,5 +84,31 @@ TEST(DenseMath, VectorOwnsAndChecksContiguousStorage) {
EXPECT_THROW(original.Axpy(1.0, Vector{2}), std::invalid_argument);
}
TEST(DenseMath, VectorRobustNormSelfAliasAndNonfinitePropagation) {
Vector large{2U, 1.0e308};
EXPECT_NEAR(large.Norm() / 1.0e308, std::sqrt(2.0), 4.0e-16);
Vector tiny{2U, 1.0e-300};
EXPECT_NEAR(tiny.Norm() / 1.0e-300, std::sqrt(2.0), 4.0e-16);
Vector values{2U};
values[0] = 2.0;
values[1] = -3.0;
values.Axpy(-2.0, values);
EXPECT_DOUBLE_EQ(values[0], -2.0);
EXPECT_DOUBLE_EQ(values[1], 3.0);
const Vector* same = &values;
values = *same;
Vector* mutable_same = &values;
values = std::move(*mutable_same);
EXPECT_DOUBLE_EQ(values.Dot(values), 13.0);
Vector nonfinite{1U, (std::numeric_limits<double>::quiet_NaN)()};
EXPECT_TRUE(std::isnan(nonfinite.Dot(Vector{1U, 1.0})));
EXPECT_TRUE(std::isnan(nonfinite.Norm()));
EXPECT_NO_THROW(nonfinite.Scale(2.0));
EXPECT_NO_THROW(nonfinite.Axpy(1.0, Vector{1U, 1.0}));
EXPECT_TRUE(std::isnan(nonfinite[0]));
EXPECT_DOUBLE_EQ(Vector{0U}.Dot(Vector{0U}), 0.0);
}
} // namespace
} // namespace fesa
+11
View File
@@ -283,3 +283,14 @@ TEST(Mitc4Geometry, ExposesTheCompleteRequiredValidationPointInventory) {
EXPECT_EQ(points[13].natural_coordinates, (Vector3{-gauss, -gauss, 0.0}));
EXPECT_EQ(points[16].natural_coordinates, (Vector3{-gauss, gauss, 0.0}));
}
TEST(Mitc4Geometry, RetainsReciprocalDeterminantOverflowGateAtTinyScale) {
const std::vector<fesa::Node> nodes{
Node(0U, {0.0, 0.0, 0.0}), Node(1U, {2e-104, 0.0, 0.0}),
Node(2U, {2e-104, 2e-104, 0.0}), Node(3U, {0.0, 2e-104, 0.0})};
EXPECT_TRUE(fesa::PreprocessShellGeometry(
nodes, {Element(10U, {0U, 1U, 2U, 3U})}, Sections(2e-99)).HasValue());
ExpectFailureCode(fesa::PreprocessShellGeometry(
nodes, {Element(10U, {0U, 1U, 2U, 3U})}, Sections(2e-102)),
"invalid-shell-jacobian");
}
@@ -133,3 +133,41 @@ TEST(SourceTargetResolver,
EXPECT_EQ(result.GetStatus().Diagnostics()[1U].message,
"Duplicate resolved source target 10.");
}
TEST(SourceTargetResolver, OwnsNodeCandidateIndexBeforeDomainCommit) {
using fesa::SourceEntityKind;
const auto index = [] {
const std::vector<fesa::Node> nodes{{{"Part-1", 20, "0020"}, {}, {}},
{{"Part-1", 10, "0010"}, {}, {}},
{{"Part-2", 10, "0010"}, {}, {}}};
const std::vector<fesa::NodeSet> sets{
{"Ends", "Part-1", {1U, 0U}, {}},
{"Ends", "Part-2", {2U}, {}},
{"Repeated", "Part-1", {0U, 1U, 0U, 1U}, {}}};
return fesa::SourceTargetIndex::FromNodeDefinitions(nodes, sets);
}();
const fesa::SourceTargetResolver resolver{index};
const auto ends =
resolver.Resolve({SourceEntityKind::kNode, "part-1", "ENDS"});
ASSERT_TRUE(ends.HasValue());
EXPECT_EQ(Indices(ends.Value()), (std::vector<fesa::EntityIndex>{1U, 0U}));
EXPECT_EQ(ends.Value()[0U].source_id.source_label_text, "0010");
EXPECT_EQ(ends.Value()[1U].source_id.source_label_text, "0020");
const auto direct =
resolver.Resolve({SourceEntityKind::kNode, "Part-2", "10"});
ASSERT_TRUE(direct.HasValue());
EXPECT_EQ(Indices(direct.Value()), (std::vector<fesa::EntityIndex>{2U}));
ExpectFailure(index, {SourceEntityKind::kNode, "", "Ends"},
"ambiguous-source-target");
const auto repeated =
resolver.Resolve({SourceEntityKind::kNode, "Part-1", "Repeated"});
ASSERT_FALSE(repeated.HasValue());
ASSERT_EQ(repeated.GetStatus().Diagnostics().size(), 2U);
EXPECT_EQ(repeated.GetStatus().Diagnostics()[0U].message,
"Duplicate resolved source target 0020.");
EXPECT_EQ(repeated.GetStatus().Diagnostics()[1U].message,
"Duplicate resolved source target 0010.");
const auto empty = fesa::SourceTargetIndex::FromNodeDefinitions({}, {});
ExpectFailure(empty, {SourceEntityKind::kNode, "", "1"},
"unresolved-source-target");
}
@@ -7,6 +7,7 @@
#include "fesa/elements/thermal_shell.h"
#include "fesa/model/domain.h"
#include "elements/thermal_shell_geometry.h"
namespace {
using fesa::Vector3;
@@ -210,3 +211,68 @@ TEST(ThermalShellGeometry,
{1, 1, 0}},
{{{0, 1, 4, 3}}, {{1, 2, 5, 4}}, {{3, 4, 7, 6}}, {{4, 5, 8, 7}}})));
}
namespace {
TEST(ThermalGeometryAction, EnclosesIndependentDyadicTangentialConstraints) {
using namespace fesa::thermal_internal;
// The rounded Gram matrix loses rank for delta=2^-40, while the physical
// tangents and their exterior determinant remain certified and nonsingular.
for (const int exponent : {-40,-27,-4,0}) {
const double delta = std::ldexp(1.,exponent);
Geometry geometry;
geometry.b = {1,1,0};
geometry.c = {1,1+delta,0};
for (std::size_t j=0; j<3; ++j) {
geometry.bi[j] = Interval{geometry.b[j]};
geometry.ci[j] = Interval{geometry.c[j]};
}
const auto evaluated = EvaluateSurface(geometry, 0, 0, Interval{0},
Interval{0}, {}, {});
ASSERT_TRUE(evaluated.HasValue()) << exponent;
const double dx[4]{-.25,.25,.25,-.25};
const double dy[4]{-.25,-.25,.25,.25};
for (std::size_t i=0; i<4; ++i) {
const double gy = (dy[i]-dx[i])/delta;
const double gx = dx[i]-gy;
const double oracle[3]{gx,gy,0};
for (std::size_t j=0; j<3; ++j) {
EXPECT_DOUBLE_EQ(evaluated.Value().gradient[i][j],oracle[j]);
const auto interval = evaluated.Value().gradient_interval[i][j];
EXPECT_LE(interval.lower,oracle[j]);
EXPECT_GE(interval.upper,oracle[j]);
}
}
}
}
TEST(ThermalGeometryAction, PreservesWholePatchAndScaledFiniteRejections) {
using namespace fesa::thermal_internal;
for (const Patch patch : {
kSquare,
Patch{{{3,-9,-3},{1,3,-3},{-1,3,3},{-3,3,3}}},
Patch{{{-1,4,-4},{-1,-2,2},{3,2,4},{-1,-4,-2}}}}) {
const auto geometry = BuildGeometry(patch, {}, {});
ASSERT_TRUE(geometry.HasValue());
for (const double xi : {-std::sqrt(.6),0.,std::sqrt(.6)})
for (const double eta : {-std::sqrt(.6),0.,std::sqrt(.6)})
EXPECT_TRUE(EvaluateSurface(geometry.Value(),xi,eta,
Interval{Down(xi),Up(xi)},Interval{Down(eta),Up(eta)},{},{}).HasValue());
}
const Patch folded{{{0,0,0},{1,1,0},{1,0,0},{0,1,0}}};
const auto rejected = BuildGeometry(folded,{"part",27,"0027"},{"mesh.inp",17});
ASSERT_FALSE(rejected.HasValue());
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].code,
"uncertain-thermal-shell-geometry");
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0].entity_identity,"part:0027");
Geometry geometry;
geometry.b = {1,0,0}; geometry.c = {0,1,0};
geometry.bi[0] = Interval{1}; geometry.ci[1] = Interval{1};
geometry.length_exponent = -600;
const auto scaled = EvaluateSurface(geometry,0,0,Interval{0},Interval{0},
{"part",27,"0027"},{"mesh.inp",17});
ASSERT_FALSE(scaled.HasValue());
EXPECT_EQ(scaled.GetStatus().Diagnostics()[0].code,
"uncertain-thermal-shell-metric");
EXPECT_EQ(scaled.GetStatus().Diagnostics()[0].message,
"Metric inverse enclosure is not finite or excludes no zero divisor.");
}
} // namespace
@@ -9,6 +9,9 @@
#include "fesa/analysis/analysis_state.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/fem/dof_manager.h"
namespace {
fesa::DofManager MakeEmptyDofs() {
@@ -6,7 +6,6 @@
#include <filesystem>
#include <limits>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
@@ -354,41 +353,6 @@ TEST(ResultRecoveryComponents,
}
}
TEST(ResultRecoveryComponents, PartitionedScaleKeepsCancelledInternalTerms) {
auto definition = MakeShellDefinition();
definition.node_sets = {{"Fixed", std::optional<std::string>{"Shell-1"},
{0U}, {definition.source_path, 60U}}};
definition.steps[0].boundaries = {
{"Fixed", 1, 1, 1.0, {definition.source_path, 61U}}};
const auto fixture = MakeShellFixture(std::move(definition));
const auto count = fixture.dofs->FullDofCount();
fesa::SparsePattern pattern;
pattern.row_offsets = {0U, 1U, 3U};
pattern.row_offsets.resize(count + 1U, 3U);
pattern.column_indices = {0U, 0U, 1U};
auto matrix = fesa::SparseMatrix::FromCoo(
count, count,
{{0U, 0U, 1.0, 0U, 0U}, {1U, 0U, -1.0, 0U, 1U},
{1U, 1U, 1.0, 0U, 2U}}, pattern);
ASSERT_TRUE(matrix.HasValue());
fesa::Vector displacement{count};
displacement[0U] = 1.0;
displacement[1U] = 1.0;
fesa::Vector external{count};
external[1U] = 1.0e-12;
const auto result = fesa::results_internal::GlobalEquilibriumRecovery(
*fixture.domain, *fixture.dofs, matrix.Value(), displacement, external);
ASSERT_TRUE(result.HasValue());
EXPECT_DOUBLE_EQ(result.Value().internal_force[1U], 0.0);
EXPECT_DOUBLE_EQ(result.Value().normalized_free_residual, 1.0e-12);
displacement[1U] = std::numeric_limits<double>::quiet_NaN();
const auto rejected = fesa::results_internal::GlobalEquilibriumRecovery(
*fixture.domain, *fixture.dofs, matrix.Value(), displacement, external);
ASSERT_FALSE(rejected.HasValue());
ExpectStatusCode(rejected.GetStatus(), "nonfinite-recovery-value");
EXPECT_DOUBLE_EQ(result.Value().reaction[1U], -1.0e-12);
}
// C-MODULE-002
TEST(ResultRecoveryComponents,
BeamResultRecoveryKeepsEndpointGaussAndStressIdentities) {
+191
View File
@@ -1,9 +1,22 @@
#include "fesa/results/results_writer.h"
#include "fesa/results/prepared_results_writer.h"
#include "fesa/results/dynamic_results_writer.h"
#include "fesa/results/eigen_results_writer.h"
#include "fesa/results/nonlinear_results_writer.h"
#include "fesa/results/solid_results_writer.h"
#include "fesa/results/thermal_results_writer.h"
#include <gtest/gtest.h>
#include <array>
#include <filesystem>
#include <stdexcept>
#include <utility>
#include <type_traits>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/analysis_state.h"
#include "fesa/core/diagnostic.h"
#include "fesa/core/status.h"
@@ -21,3 +34,181 @@ static_assert(
std::is_same_v<decltype(&fesa::ResultsWriter::Write), WriteSignature>);
} // namespace
namespace {
// These adapters only observe borrowed values; no procedure, kernel, or file I/O.
struct WriterCalls {
std::vector<const std::filesystem::path*> paths;
std::vector<const fesa::Domain*> domains;
std::vector<const std::vector<fesa::Diagnostic>*> diagnostics;
std::vector<unsigned> families;
fesa::Status Record(unsigned family, const std::filesystem::path& path,
const fesa::Domain& domain,
const std::vector<fesa::Diagnostic>& values) {
paths.push_back(&path);
domains.push_back(&domain);
diagnostics.push_back(&values);
families.push_back(family);
return fesa::Status::Failure(
fesa::FailureCategory::kOutput,
{{fesa::Severity::kError, "candidate-close-failure", {"source.inp", 7U},
"WRITER", "0007", "Preserved writer failure."}});
}
};
template <class Writer, class State, unsigned Family>
class RecordingWriter final : public Writer {
public:
explicit RecordingWriter(WriterCalls& calls) : calls_{calls} {}
fesa::Status Write(const std::filesystem::path& path,
const fesa::Domain& domain, const State& state,
const std::vector<fesa::Diagnostic>& diagnostics) override {
received = &state;
return calls_.Record(Family, path, domain, diagnostics);
}
const State* received{nullptr};
private:
WriterCalls& calls_;
};
class RecordingSolidWriter final : public fesa::SolidResultsWriter {
public:
explicit RecordingSolidWriter(WriterCalls& calls) : calls_{calls} {}
fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d,
const fesa::SolidStaticState& s,
const std::vector<fesa::Diagnostic>& v) override {
static_state = &s;
return calls_.Record(5U, p, d, v);
}
fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d,
const fesa::SolidFrequencyState& s,
const std::vector<fesa::Diagnostic>& v) override {
frequency_state = &s;
return calls_.Record(6U, p, d, v);
}
fesa::Status Write(const std::filesystem::path& p, const fesa::Domain& d,
const fesa::SolidDynamicAnalysisState& s,
const std::vector<fesa::Diagnostic>& v) override {
dynamic_state = &s;
return calls_.Record(7U, p, d, v);
}
const fesa::SolidStaticState* static_state{nullptr};
const fesa::SolidFrequencyState* frequency_state{nullptr};
const fesa::SolidDynamicAnalysisState* dynamic_state{nullptr};
private:
WriterCalls& calls_;
};
// Empty values intentionally bypass physics: dispatch must preserve their type.
std::vector<fesa::PreparedAnalysisOutcome> EmptyOutcomes() {
std::vector<fesa::PreparedAnalysisOutcome> values;
values.emplace_back(fesa::LinearStaticExecutionResult{
fesa::AnalysisState::Create(fesa::DofManager{}, {"source/step", 9U}), {}});
fesa::ModelDefinition definition{};
definition.steps = {{"value-only", {}, {}, 0.1, 1.0, 0.01, 1.0, {}}};
auto domain = fesa::Domain::Create(std::move(definition));
auto model = fesa::AnalysisModel::Create(domain.Value());
auto dofs = fesa::DofManager::Create(model.Value());
auto nonlinear = fesa::NonlinearAnalysisState::Create(
domain.Value(), dofs.Value(), {});
if (!nonlinear.HasValue())
throw std::runtime_error{"Empty nonlinear value construction failed."};
values.emplace_back(
fesa::NonlinearStaticExecutionResult{std::move(nonlinear.Value()), {}});
values.emplace_back(fesa::EigenAnalysisState{
fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}});
values.emplace_back(fesa::DynamicExecutionResult{});
values.emplace_back(fesa::SolidStaticExecutionResult{});
values.emplace_back(fesa::SolidFrequencyState{
fesa::FeastEigenResult{0U, {}, fesa::Matrix{0U, 0U}}});
values.emplace_back(fesa::SolidDynamicExecutionResult{});
return values;
}
} // namespace
TEST(PreparedResultsWriter, RoutesValueOutcomesAndPreservesWriterFailure) {
auto domain = fesa::Domain::Create({});
ASSERT_TRUE(domain.HasValue());
const std::filesystem::path output{"relative/custom-result.h5"};
const std::vector<fesa::Diagnostic> diagnostics{
{fesa::Severity::kWarning, "original-warning", {"source.inp", 3U},
"NODE", "0003", "Original diagnostic."}};
WriterCalls calls;
RecordingWriter<fesa::ResultsWriter, fesa::AnalysisState, 0U> linear{calls};
RecordingWriter<fesa::NonlinearResultsWriter, fesa::NonlinearAnalysisState, 1U>
nonlinear{calls};
RecordingWriter<fesa::EigenResultsWriter, fesa::EigenAnalysisState, 2U>
eigen{calls};
RecordingWriter<fesa::DynamicResultsWriter, fesa::DynamicAnalysisState, 3U>
dynamic{calls};
RecordingWriter<fesa::ThermalResultsWriter, fesa::ThermalAnalysisState, 4U>
thermal{calls};
RecordingSolidWriter solid{calls};
auto outcomes = EmptyOutcomes();
const std::array<unsigned, 7> families{0U, 1U, 2U, 3U, 5U, 6U, 7U};
ASSERT_EQ(outcomes.size(), families.size());
for (std::size_t index = 0U; index < outcomes.size(); ++index) {
const auto status = fesa::WritePreparedAnalysisResult(
output, domain.Value(), outcomes[index], diagnostics, linear,
nonlinear, eigen, &dynamic, &thermal, &solid);
ASSERT_EQ(calls.families.size(), index + 1U);
EXPECT_EQ(calls.families.back(), families[index]);
EXPECT_EQ(calls.paths.back(), &output);
EXPECT_EQ(calls.domains.back(), &domain.Value());
EXPECT_EQ(calls.diagnostics.back(), &diagnostics);
EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "candidate-close-failure");
EXPECT_EQ(status.Diagnostics()[0U].location.file, "source.inp");
EXPECT_EQ(status.Diagnostics()[0U].location.line, 7U);
EXPECT_EQ(status.Diagnostics()[0U].keyword, "WRITER");
EXPECT_EQ(status.Diagnostics()[0U].entity_identity, "0007");
EXPECT_EQ(status.Diagnostics()[0U].message, "Preserved writer failure.");
}
EXPECT_EQ(linear.received,
&std::get<fesa::LinearStaticExecutionResult>(outcomes[0U]).state);
EXPECT_EQ(nonlinear.received,
&std::get<fesa::NonlinearStaticExecutionResult>(outcomes[1U]).state);
EXPECT_EQ(eigen.received, &std::get<fesa::EigenAnalysisState>(outcomes[2U]));
EXPECT_EQ(dynamic.received,
&std::get<fesa::DynamicExecutionResult>(outcomes[3U]).state);
EXPECT_EQ(thermal.received, nullptr);
EXPECT_EQ(solid.static_state,
&std::get<fesa::SolidStaticExecutionResult>(outcomes[4U]).state);
EXPECT_EQ(solid.frequency_state,
&std::get<fesa::SolidFrequencyState>(outcomes[5U]));
EXPECT_EQ(solid.dynamic_state,
&std::get<fesa::SolidDynamicExecutionResult>(outcomes[6U]).state);
}
TEST(PreparedResultsWriter, MissingOptionalAdapterInvokesNoOtherWriter) {
auto domain = fesa::Domain::Create({});
ASSERT_TRUE(domain.HasValue());
WriterCalls calls;
RecordingWriter<fesa::ResultsWriter, fesa::AnalysisState, 0U> linear{calls};
RecordingWriter<fesa::NonlinearResultsWriter, fesa::NonlinearAnalysisState, 1U>
nonlinear{calls};
RecordingWriter<fesa::EigenResultsWriter, fesa::EigenAnalysisState, 2U>
eigen{calls};
auto outcomes = EmptyOutcomes();
const std::array<const char*, 4> codes{
"missing-dynamic-writer", "missing-solid-writer",
"missing-solid-writer", "missing-solid-writer"};
const std::array<const char*, 4> keywords{"DYNAMIC", "", "", ""};
for (std::size_t index = 3U; index < outcomes.size(); ++index) {
const auto status = fesa::WritePreparedAnalysisResult(
"explicit.h5", domain.Value(), outcomes[index], {}, linear, nonlinear,
eigen);
EXPECT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, codes[index - 3U]);
EXPECT_EQ(status.Diagnostics()[0U].keyword, keywords[index - 3U]);
EXPECT_TRUE(calls.families.empty());
}
}
@@ -2,13 +2,22 @@
#include <gtest/gtest.h>
#include <limits>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
#include "fesa/analysis/linear_frequency_procedure.h"
#include "fesa/analysis/solid_frequency_procedure.h"
#include "fesa/solvers/eigen/eigen_solver.h"
#include "fesa/solvers/eigen/mkl_feast_eigen_solver.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/sparse_matrix.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "solvers/linear/pardiso_session_test_seam.h"
namespace {
@@ -125,3 +134,277 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
EXPECT_EQ(no_diagonal_status.Diagnostics().front().code,
"solver-missing-diagonal");
}
namespace {
class RecordingEigenSolver final : public fesa::EigenSolver {
public:
explicit RecordingEigenSolver(bool& destroyed) : destroyed_(destroyed) {}
~RecordingEigenSolver() override { destroyed_ = true; }
fesa::Result<fesa::FeastEigenResult> Solve(
const fesa::SparseMatrix& matrix, std::size_t modes,
double scale) const override {
EXPECT_EQ(matrix.Rows(), 0U);
EXPECT_EQ(modes, 3U);
EXPECT_DOUBLE_EQ(scale, 7.0);
++standard_calls;
return fesa::Result<fesa::FeastEigenResult>::Failure(
fesa::Status::Failure(fesa::FailureCategory::kSolver, {}));
}
fesa::Result<fesa::FeastEigenResult> SolveGeneralized(
const fesa::SparseMatrix& stiffness, const fesa::SparseMatrix& mass,
std::size_t modes, double lower_bound,
double scale) const override {
EXPECT_EQ(stiffness.Rows(), 0U);
EXPECT_EQ(mass.Rows(), 0U);
EXPECT_EQ(modes, 4U);
EXPECT_DOUBLE_EQ(lower_bound, 2.0);
EXPECT_DOUBLE_EQ(scale, 11.0);
++generalized_calls;
return fesa::Result<fesa::FeastEigenResult>::Failure(
fesa::Status::Failure(fesa::FailureCategory::kSolver, {}));
}
mutable int standard_calls{0};
mutable int generalized_calls{0};
private:
bool& destroyed_;
};
} // namespace
TEST(EigenSolverContract, DispatchesBothPencilsAndDestroysThroughBase) {
static_assert(std::is_abstract_v<fesa::EigenSolver>);
static_assert(std::has_virtual_destructor_v<fesa::EigenSolver>);
static_assert(std::is_base_of_v<fesa::EigenSolver,
fesa::MklFeastEigenSolver>);
static_assert(std::is_constructible_v<fesa::LinearFrequencyProcedureExecutor,
const fesa::EigenSolver&>);
static_assert(std::is_constructible_v<fesa::SolidFrequencyProcedureExecutor,
const fesa::EigenSolver&>);
bool destroyed = false;
{
auto recording = std::make_unique<RecordingEigenSolver>(destroyed);
const auto* observations = recording.get();
std::unique_ptr<fesa::EigenSolver> solver = std::move(recording);
const fesa::EigenSolver& contract = *solver;
const auto empty = MakeDenseCsr(0U, 0U, {});
EXPECT_FALSE(contract.Solve(empty, 3U, 7.0).HasValue());
EXPECT_FALSE(contract.SolveGeneralized(empty, empty, 4U, 2.0, 11.0)
.HasValue());
EXPECT_EQ(observations->standard_calls, 1);
EXPECT_EQ(observations->generalized_calls, 1);
EXPECT_FALSE(destroyed);
}
EXPECT_TRUE(destroyed);
}
TEST(PardisoSession, RetainsDistinctStoragePoliciesAndFreshPhaseOrder) {
std::vector<int> phases;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT&, MKL_INT&) {
phases.push_back(static_cast<int>(call.phase));
if (call.phase == 11) {
EXPECT_EQ(call.equation_count, 2);
if (call.matrix_type == 2) {
EXPECT_EQ(call.row_offsets[2], 3);
EXPECT_EQ(call.column_indices[0], 0);
EXPECT_EQ(call.column_indices[1], 1);
EXPECT_EQ(call.column_indices[2], 1);
} else {
EXPECT_EQ(call.matrix_type, 11);
EXPECT_EQ(call.row_offsets[2], 4);
EXPECT_DOUBLE_EQ(call.values[2], 0.0);
}
}
}};
{
fesa::MklPardisoSolver solver;
const auto matrix = MakeDenseCsr(2U, 2U, {4.0, 1.0, 1.0, 3.0});
ASSERT_TRUE(solver.Factorize(matrix).IsOk());
fesa::Vector solution{2U};
ASSERT_TRUE(solver.Solve(fesa::Vector{2U, 1.0}, solution).IsOk());
EXPECT_NEAR(solution[0], 2.0 / 11.0, 1e-14);
ASSERT_TRUE(solver.Solve(fesa::Vector{2U, 2.0}, solution).IsOk());
EXPECT_NEAR(solution[1], 6.0 / 11.0, 1e-14);
ASSERT_TRUE(solver.Factorize(matrix).IsOk());
}
EXPECT_EQ(phases, (std::vector<int>{11, 22, 33, 33, -1, 11, 22, -1}));
phases.clear();
{
fesa::MklPardisoTangentSolver solver;
const auto matrix = MakeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0});
ASSERT_TRUE(solver.FactorizeFresh(matrix).IsOk());
fesa::Vector correction{2U};
ASSERT_TRUE(solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction).IsOk());
EXPECT_NEAR(correction[0], 2.0, 1e-14);
EXPECT_NEAR(correction[1], 2.0, 1e-14);
ASSERT_TRUE(solver.FactorizeFresh(matrix).IsOk());
}
EXPECT_EQ(phases, (std::vector<int>{11, 22, 33, -1, 11, 22, -1}));
}
TEST(PardisoSession, FailedSolvePreservesOutputAndRetainedFactorization) {
int solve_calls = 0;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT& error, MKL_INT&) {
if (call.phase != 33) return;
++solve_calls;
if (solve_calls == 1) error = -3;
if (solve_calls == 2)
call.solution[0] = (std::numeric_limits<double>::infinity)();
}};
fesa::MklPardisoSolver solver;
ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, 1U, {2.0})).IsOk());
fesa::Vector solution{1U, 17.0};
const auto failed = solver.Solve(fesa::Vector{1U, 6.0}, solution);
ASSERT_FALSE(failed.IsOk());
EXPECT_EQ(failed.Diagnostics().front().code, "pardiso-solve-failed");
EXPECT_DOUBLE_EQ(solution[0], 17.0);
const auto nonfinite = solver.Solve(fesa::Vector{1U, 6.0}, solution);
ASSERT_FALSE(nonfinite.IsOk());
EXPECT_EQ(nonfinite.Diagnostics().front().code, "nonfinite-solver-solution");
EXPECT_DOUBLE_EQ(solution[0], 17.0);
ASSERT_TRUE(solver.Solve(fesa::Vector{1U, 6.0}, solution).IsOk());
EXPECT_DOUBLE_EQ(solution[0], 3.0);
}
TEST(PardisoSession, ReleaseFailurePrecedesNewValidationAndInvalidatesState) {
int releases = 0;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT& error, MKL_INT&) {
if (call.phase == -1) { ++releases; error = -3; }
}};
fesa::MklPardisoSolver solver;
ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, 1U, {2.0})).IsOk());
const auto status = solver.Factorize(MakeDenseCsr(1U, 2U, {1.0, 2.0}));
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Diagnostics().front().code, "pardiso-release-failed");
fesa::Vector output{1U, 19.0};
const auto unavailable = solver.Solve(fesa::Vector{1U, 1.0}, output);
ASSERT_FALSE(unavailable.IsOk());
EXPECT_EQ(unavailable.Diagnostics().front().code, "solver-not-factorized");
EXPECT_DOUBLE_EQ(output[0], 19.0);
EXPECT_EQ(releases, 1);
}
TEST(PardisoSession, FactorizationFailuresAndExceptionsReleaseExactlyOnce) {
const auto matrix = MakeDenseCsr(1U, 1U, {2.0});
for (const int failing_phase : {11, 22}) {
std::vector<int> phases;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT& error, MKL_INT&) {
phases.push_back(static_cast<int>(call.phase));
if (call.phase == failing_phase) error = -8;
}};
{
fesa::MklPardisoSolver solver;
const auto status = solver.Factorize(matrix);
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Diagnostics().front().code, "pardiso-integer-overflow");
fesa::Vector output{1U, 29.0};
EXPECT_FALSE(solver.Solve(fesa::Vector{1U, 1.0}, output).IsOk());
EXPECT_DOUBLE_EQ(output[0], 29.0);
}
EXPECT_EQ(phases, failing_phase == 11 ? (std::vector<int>{11, -1})
: (std::vector<int>{11, 22, -1}));
}
std::vector<int> phases;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT&, MKL_INT&) {
phases.push_back(static_cast<int>(call.phase));
if (call.phase == 11) throw std::runtime_error("injected unwind");
}};
{
fesa::MklPardisoTangentSolver solver;
EXPECT_THROW(solver.FactorizeFresh(matrix), std::runtime_error);
EXPECT_EQ(phases, (std::vector<int>{11, -1}));
}
EXPECT_EQ(phases, (std::vector<int>{11, -1}));
}
TEST(PardisoSession, FailedTangentSolveRetainsStateButFailedFreshInvalidatesIt) {
bool fail_solve = true;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT& error, MKL_INT&) {
if (call.phase == 33 && fail_solve) error = -3;
}};
fesa::MklPardisoTangentSolver solver;
ASSERT_TRUE(solver.FactorizeFresh(
MakeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0})).IsOk());
fesa::Vector correction{2U, 31.0};
auto status = solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction);
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Diagnostics().front().code, "pardiso-solve-failed");
EXPECT_DOUBLE_EQ(correction[0], 31.0);
EXPECT_DOUBLE_EQ(correction[1], 31.0);
fail_solve = false;
ASSERT_TRUE(solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction).IsOk());
EXPECT_NEAR(correction[0], 2.0, 1e-14);
EXPECT_NEAR(correction[1], 2.0, 1e-14);
status = solver.FactorizeFresh(MakeDenseCsr(1U, 2U, {1.0, 2.0}));
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Diagnostics().front().code, "solver-matrix-not-square");
status = solver.SolveCorrection(fesa::Vector{2U, 6.0}, correction);
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Diagnostics().front().code, "solver-not-factorized");
EXPECT_NEAR(correction[0], 2.0, 1e-14);
EXPECT_NEAR(correction[1], 2.0, 1e-14);
}
TEST(PardisoSession, EmptyLifecycleNeverInvokesBackend) {
int calls = 0;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall&, MKL_INT&, MKL_INT&) {
++calls;
}};
{
fesa::MklPardisoSolver solver;
fesa::MklPardisoTangentSolver tangent;
const auto empty = MakeDenseCsr(0U, 0U, {});
fesa::Vector solution{0U};
ASSERT_TRUE(solver.Factorize(empty).IsOk());
ASSERT_TRUE(solver.Solve(fesa::Vector{0U}, solution).IsOk());
ASSERT_TRUE(solver.Factorize(empty).IsOk());
ASSERT_TRUE(tangent.FactorizeFresh(empty).IsOk());
ASSERT_TRUE(tangent.SolveCorrection(fesa::Vector{0U}, solution).IsOk());
ASSERT_TRUE(tangent.FactorizeFresh(empty).IsOk());
}
EXPECT_EQ(calls, 0);
}
TEST(PardisoSession, RestoresDefaultThreadPolicyAndPreservesCallerOverride) {
const int previous = mkl_set_num_threads_local(0);
struct RestoreThreads {
int previous;
~RestoreThreads() { mkl_set_num_threads_local(previous); }
} restore{previous};
const auto matrix = MakeDenseCsr(1U, 1U, {2.0});
for (const int caller_threads : {0, 2}) {
mkl_set_num_threads_local(caller_threads);
int release_calls = 0;
fesa::internal::ScopedPardisoBackendTestHook hook{
[&](const fesa::internal::PardisoBackendTestCall& call,
MKL_INT& error, MKL_INT&) {
EXPECT_EQ(mkl_get_max_threads(), caller_threads == 0 ? 1 : 2);
if (call.phase == 22) error = -8;
if (call.phase == -1) ++release_calls;
}};
{
fesa::MklPardisoSolver solver;
EXPECT_FALSE(solver.Factorize(matrix).IsOk());
EXPECT_EQ(mkl_set_num_threads_local(caller_threads), caller_threads);
}
EXPECT_EQ(release_calls, 1);
EXPECT_EQ(mkl_set_num_threads_local(caller_threads), caller_threads);
}
}
@@ -3,7 +3,6 @@
#include <gtest/gtest.h>
#include <cmath>
#include <future>
#include <initializer_list>
#include <limits>
#include <string>
@@ -12,7 +11,6 @@
#include "fesa/fem/dof_manager.h"
#include "fesa/math/sparse_matrix.h"
#include "math/dense_blas_internal.h"
namespace {
@@ -125,42 +123,6 @@ TEST(MklPardisoSolver, ReusesOneFactorizationForRepeatedRhs) {
EXPECT_LE(NormalizedResidual(matrix, second, rhs_second), 1.0e-10);
}
TEST(MklPardisoSolver, SingleEquationPolicyPreservesLifecycleAndConcurrentTls) {
const auto worker = [] {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
fesa::MklPardisoSolver solver;
for (int repeat = 0; repeat < 10; ++repeat) {
if (!solver.Factorize(MakeDenseCsr(1U, {2.0})).IsOk()) return false;
auto solution = MakeVector({9.0});
if (!solver.Solve(MakeVector({4.0}), solution).IsOk() ||
solution[0U] != 2.0) return false;
if (!solver.Factorize(MakeDenseCsr(1U, {4.0})).IsOk()) return false;
if (!solver.Solve(MakeVector({4.0}), solution).IsOk() ||
solution[0U] != 1.0) return false;
auto invalid_rhs = MakeVector({std::numeric_limits<double>::quiet_NaN()});
if (solver.Solve(invalid_rhs, solution).IsOk() || solution[0U] != 1.0)
return false;
if (!solver.Solve(MakeVector({8.0}), solution).IsOk() ||
solution[0U] != 2.0) return false;
if (solver.Factorize(MakeDenseCsr(1U, {-1.0})).IsOk()) return false;
if (solver.Solve(MakeVector({8.0}), solution).IsOk() ||
solution[0U] != 2.0) return false;
if (mkl_get_max_threads() != default_threads ||
mkl_set_num_threads_local(0) != 0) return false;
}
return true;
};
auto first = std::async(std::launch::async, worker);
auto second = std::async(std::launch::async, worker);
EXPECT_TRUE(first.get());
EXPECT_TRUE(second.get());
}
TEST(MklPardisoSolver, StillRejectsNonsymmetricPublicCsr) {
const auto nonsymmetric = MakeDenseCsr(2U, {2.0, 1.0, 0.0, 3.0});
fesa::MklPardisoSolver solver;
@@ -193,32 +155,6 @@ TEST(MklPardisoSolver, RefactorizesWithoutLeakingState) {
1.0e-10);
}
TEST(MklPardisoSolver, SingleEquationScopeTransitionsBetweenRetainedSizes) {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
fesa::MklPardisoSolver solver;
ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, {2.0})).IsOk());
auto single = MakeVector({0.0});
ASSERT_TRUE(solver.Solve(MakeVector({4.0}), single).IsOk());
EXPECT_DOUBLE_EQ(single[0U], 2.0);
ASSERT_TRUE(solver.Factorize(MakeDenseCsr(2U, {2, 0, 0, 3})).IsOk());
fesa::Vector pair{2U};
ASSERT_TRUE(solver.Solve(MakeVector({4, 6}), pair).IsOk());
EXPECT_NEAR(pair[0U], 2, 1e-14);
EXPECT_NEAR(pair[1U], 2, 1e-14);
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
ASSERT_TRUE(solver.Factorize(MakeDenseCsr(1U, {4.0})).IsOk());
ASSERT_TRUE(solver.Solve(MakeVector({8.0}), single).IsOk());
EXPECT_DOUBLE_EQ(single[0U], 2.0);
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
}
TEST(MklPardisoSolver, ClassifiesSingularIndefiniteAndNonfiniteFailures) {
fesa::MklPardisoSolver solver;
@@ -3,7 +3,6 @@
#include <gtest/gtest.h>
#include <cstddef>
#include <future>
#include <initializer_list>
#include <limits>
#include <type_traits>
@@ -14,7 +13,6 @@
#include "fesa/math/sparse_matrix.h"
#include "fesa/math/vector.h"
#include "fesa/solvers/linear/tangent_solver.h"
#include "math/dense_blas_internal.h"
namespace {
@@ -131,104 +129,4 @@ TEST(TangentSolver, RejectsSolveBeforeSuccessfulFactorization) {
EXPECT_DOUBLE_EQ(correction[1U], -9.0);
}
TEST(TangentSolver, ReusesRhsButFreshValuesAndFailureCannotReuseStaleState) {
fesa::MklPardisoTangentSolver solver;
ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {2.0, 1.0, 0.0, 3.0}))
.IsOk());
fesa::Vector correction{2U};
for (double scale : {1.0, -2.0, 0.5}) {
ASSERT_TRUE(solver.SolveCorrection(MakeVector({4.0 * scale, 6.0 * scale}),
correction).IsOk());
EXPECT_NEAR(correction[0U], scale, 1.0e-14);
EXPECT_NEAR(correction[1U], 2.0 * scale, 1.0e-14);
}
const auto bad_rhs = MakeVector({std::numeric_limits<double>::quiet_NaN(), 0});
correction = MakeVector({17, -4});
EXPECT_FALSE(solver.SolveCorrection(bad_rhs, correction).IsOk());
EXPECT_DOUBLE_EQ(correction[0U], 17);
EXPECT_DOUBLE_EQ(correction[1U], -4);
ASSERT_TRUE(solver.SolveCorrection(MakeVector({4, 6}), correction).IsOk());
ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {4, 0, 1, 2})).IsOk());
ASSERT_TRUE(solver.SolveCorrection(MakeVector({4, 5}), correction).IsOk());
EXPECT_NEAR(correction[0U], 1, 1.0e-14);
EXPECT_NEAR(correction[1U], 2, 1.0e-14);
auto diagonal = fesa::SparseMatrix::FromCoo(
2U, 2U, {{0U, 0U, 5, 0U, 0U}, {1U, 1U, 7, 1U, 0U}},
{{0U, 1U, 2U}, {0U, 1U}});
ASSERT_TRUE(diagonal.HasValue());
ASSERT_TRUE(solver.FactorizeFresh(diagonal.Value()).IsOk());
ASSERT_TRUE(solver.SolveCorrection(MakeVector({10, -7}), correction).IsOk());
EXPECT_NEAR(correction[0U], 2, 1.0e-14);
EXPECT_NEAR(correction[1U], -1, 1.0e-14);
EXPECT_FALSE(solver.FactorizeFresh(MakeDenseCsr(2U, {0, 0, 0, 0})).IsOk());
correction = MakeVector({19, -8});
const auto stale = solver.SolveCorrection(MakeVector({10, -7}), correction);
ASSERT_FALSE(stale.IsOk());
EXPECT_EQ(stale.Diagnostics().front().code, "solver-not-factorized");
EXPECT_DOUBLE_EQ(correction[0U], 19);
EXPECT_DOUBLE_EQ(correction[1U], -8);
ASSERT_TRUE(solver.FactorizeFresh(diagonal.Value()).IsOk());
EXPECT_TRUE(solver.SolveCorrection(MakeVector({10, -7}), correction).IsOk());
}
TEST(TangentSolver, SingleEquationScopeRestoresTlsAcrossFreshFailureAndReuse) {
const auto worker = [] {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
fesa::MklPardisoTangentSolver solver;
for (int repeat = 0; repeat < 10; ++repeat) {
auto correction = MakeVector({17.0});
if (!solver.FactorizeFresh(MakeDenseCsr(1U, {2.0})).IsOk()) return false;
if (!solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() ||
correction[0U] != 2.0) return false;
if (!solver.FactorizeFresh(MakeDenseCsr(1U, {4.0})).IsOk()) return false;
if (!solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() ||
correction[0U] != 1.0) return false;
if (solver.SolveCorrection(MakeVector({
std::numeric_limits<double>::infinity()}), correction).IsOk() ||
correction[0U] != 1.0) return false;
if (solver.FactorizeFresh(MakeDenseCsr(1U, {0.0})).IsOk()) return false;
if (solver.SolveCorrection(MakeVector({4.0}), correction).IsOk() ||
correction[0U] != 1.0) return false;
if (mkl_get_max_threads() != default_threads ||
mkl_set_num_threads_local(0) != 0) return false;
}
return true;
};
auto first = std::async(std::launch::async, worker);
auto second = std::async(std::launch::async, worker);
EXPECT_TRUE(first.get());
EXPECT_TRUE(second.get());
}
TEST(TangentSolver, SingleEquationScopeTransitionsBetweenRetainedSizes) {
const int previous = mkl_set_num_threads_local(0);
struct Restore {
int previous;
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
} restore{previous};
const int default_threads = mkl_get_max_threads();
fesa::MklPardisoTangentSolver solver;
ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(1U, {2.0})).IsOk());
auto single = MakeVector({0.0});
ASSERT_TRUE(solver.SolveCorrection(MakeVector({4.0}), single).IsOk());
EXPECT_DOUBLE_EQ(single[0U], 2.0);
ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(2U, {2, 1, 0, 3})).IsOk());
fesa::Vector pair{2U};
ASSERT_TRUE(solver.SolveCorrection(MakeVector({6, 6}), pair).IsOk());
EXPECT_NEAR(pair[0U], 2, 1e-14);
EXPECT_NEAR(pair[1U], 2, 1e-14);
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
ASSERT_TRUE(solver.FactorizeFresh(MakeDenseCsr(1U, {4.0})).IsOk());
ASSERT_TRUE(solver.SolveCorrection(MakeVector({8.0}), single).IsOk());
EXPECT_DOUBLE_EQ(single[0U], 2.0);
EXPECT_EQ(mkl_get_max_threads(), default_threads);
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
}
} // namespace