Integrate Refactor solver sources and dual MSVC CMake builds
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
@@ -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>
|
||||
|
||||
@@ -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, ¶meters); 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, ¶meters); 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, ¶meters); 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, ¶meters); 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);
|
||||
}
|
||||
|
||||
@@ -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(¤t.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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"}));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()}) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user