feat: adopt lumped mass and reduced-coordinate shell HHT
Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
This commit is contained in:
@@ -14,6 +14,7 @@ add_executable(
|
||||
unit/analysis/hht_energy_verification_test.cpp
|
||||
unit/analysis/hht_temporal_verification_test.cpp
|
||||
unit/analysis/hht_dynamic_algebra_test.cpp
|
||||
unit/analysis/hht_reduced_shell_test.cpp
|
||||
unit/analysis/hht_dynamic_procedure_test.cpp
|
||||
unit/analysis/nonlinear_analysis_state_test.cpp
|
||||
unit/analysis/nonlinear_static_control_test.cpp
|
||||
|
||||
@@ -138,7 +138,7 @@ def make_result(path, model):
|
||||
"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": "consistent-reference", "source_step_name": "Step-1",
|
||||
"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",
|
||||
@@ -430,6 +430,7 @@ def test_wrong_result_node_beats_nonfinite_displacement(tmp_path, result_fixture
|
||||
@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):
|
||||
@@ -535,13 +536,94 @@ def test_report_is_deterministic_and_keeps_family_boundaries(result_fixtures):
|
||||
assert len({row["reference_sha256"] for row in first["families"]}) == 3
|
||||
|
||||
|
||||
def test_modal_original_absence_is_not_pass(result_fixtures):
|
||||
report = compare_fixture(result_fixtures["hexa_freq"], "hexa_freq")
|
||||
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")])
|
||||
|
||||
@@ -37,7 +37,7 @@ def artifacts(tmp_path_factory):
|
||||
metadata = file.create_group("metadata")
|
||||
metadata.attrs.update({
|
||||
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
|
||||
"mass_formulation": "consistent-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
|
||||
"mass_formulation": "nodal-lumped-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
|
||||
"initial_state": "zero-q-v-step-load-0+",
|
||||
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
|
||||
"source_step_name": "Step-1", "step_name": "Step-1", "unit_system_label": "consistent-input-units",
|
||||
@@ -71,6 +71,55 @@ def changed_result(tmp_path, artifacts):
|
||||
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)
|
||||
@@ -128,7 +177,7 @@ def test_hdf5_precheck_rejects_before_tolerance(tmp_path, artifacts, mutation, c
|
||||
if mutation in {"procedure", "mass", "coefficient", "initial_convention", "input_content"}:
|
||||
key, value = {
|
||||
"procedure": ("analysis_procedure", "linear-static"),
|
||||
"mass": ("mass_formulation", "lumped"),
|
||||
"mass": ("mass_formulation", "consistent-kinetic"),
|
||||
"coefficient": ("alpha", 0.0),
|
||||
"initial_convention": ("initial_state", "unloaded"),
|
||||
"input_content": ("source_input_identity", "wrong-content"),
|
||||
|
||||
@@ -140,6 +140,23 @@ fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
|
||||
return frame;
|
||||
}
|
||||
|
||||
TEST(DynamicAnalysisState, RejectsUnknownOrChangingIntegrationFormulation) {
|
||||
auto fixture = MakeFixture();
|
||||
auto initial = MakeFrame(*fixture.domain, 0U);
|
||||
initial.time_integration_formulation =
|
||||
static_cast<fesa::DynamicTimeIntegrationFormulation>(99);
|
||||
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
|
||||
EXPECT_TRUE(fixture.state.AcceptedFrames().empty());
|
||||
initial.time_integration_formulation =
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(initial).IsOk());
|
||||
auto next = MakeFrame(*fixture.domain, 1U);
|
||||
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(next).IsOk());
|
||||
EXPECT_EQ(fixture.state.AcceptedFrames().size(), 1U);
|
||||
next.time_integration_formulation = initial.time_integration_formulation;
|
||||
EXPECT_TRUE(fixture.state.CommitAcceptedFrame(next).IsOk());
|
||||
}
|
||||
|
||||
TEST(DynamicAnalysisState, DefaultStateRejectsTrialAndCommit) {
|
||||
fesa::DynamicAnalysisState state;
|
||||
EXPECT_FALSE(state.IsInitialized());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -11,6 +13,7 @@
|
||||
|
||||
#include "analysis/hht_dynamic_internal.h"
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
@@ -24,7 +27,8 @@ struct Fixture {
|
||||
|
||||
Fixture MakeFixture(const std::size_t free_nodes = 1U,
|
||||
const bool partial_rotation = false,
|
||||
const bool translations_free = true) {
|
||||
const bool translations_free = true,
|
||||
const double youngs_modulus = 1000.0) {
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = "hht-algebra.inp";
|
||||
definition.nodes = {
|
||||
@@ -32,7 +36,8 @@ Fixture MakeFixture(const std::size_t free_nodes = 1U,
|
||||
{{"Plate", 2, "2"}, {1, -1, 0}, {definition.source_path, 2U}},
|
||||
{{"Plate", 3, "3"}, {1, 1, 0}, {definition.source_path, 3U}},
|
||||
{{"Plate", 4, "4"}, {-1, 1, 0}, {definition.source_path, 4U}}};
|
||||
definition.materials = {{"Elastic", 1000, 0.3, {definition.source_path, 5U}}};
|
||||
definition.materials = {
|
||||
{"Elastic", youngs_modulus, 0.3, {definition.source_path, 5U}}};
|
||||
definition.densities = {{0U, 1.0, {definition.source_path, 6U}}};
|
||||
definition.shell_sections = {
|
||||
{"Plate", 0.2, 0U, {definition.source_path, 7U}}};
|
||||
@@ -106,8 +111,11 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
frame.external_force[0] = 2.0;
|
||||
frame.residual_scale[0] = 2.0;
|
||||
fesa::Matrix mass{24U, 24U};
|
||||
fesa::Matrix position{24U, 24U};
|
||||
fesa::Matrix velocity{24U, 24U};
|
||||
fesa::Matrix stiffness{24U, 24U};
|
||||
fesa::Matrix metric{24U, 24U};
|
||||
fesa::Matrix hidden_gradient{4U, 24U};
|
||||
std::vector<fesa::DynamicRotationKinematics> rotations;
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
const std::size_t base = 6U * i;
|
||||
@@ -119,12 +127,16 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
stiffness(base + 5U, base) = -0.7 * k;
|
||||
stiffness(base + 5U, base + 5U) = k;
|
||||
metric(base + 5U, base + 5U) = 3.0;
|
||||
hidden_gradient(i, base) = -0.7 * k;
|
||||
hidden_gradient(i, base + 5U) = k;
|
||||
auto rotation = fesa::EvaluateDynamicRotation({}, {0, 0, 1}, {}, {});
|
||||
EXPECT_TRUE(rotation.HasValue());
|
||||
rotations.push_back(std::move(rotation.Value()));
|
||||
}
|
||||
return {std::move(frame),
|
||||
Sparse(mass),
|
||||
Sparse(position),
|
||||
Sparse(velocity),
|
||||
Sparse(stiffness),
|
||||
Sparse(stiffness),
|
||||
Sparse(metric),
|
||||
@@ -134,7 +146,318 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
0.0,
|
||||
0.0,
|
||||
fesa::Vector{4U},
|
||||
fesa::Vector{4U}};
|
||||
fesa::Vector{4U},
|
||||
Sparse(hidden_gradient)};
|
||||
}
|
||||
|
||||
fesa::DynamicAcceptedFrameState EmptyTrial(const Fixture& fixture) {
|
||||
fesa::DynamicAcceptedFrameState frame{};
|
||||
for (auto* values :
|
||||
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
|
||||
&frame.algorithmic_acceleration, &frame.external_force})
|
||||
*values = fesa::Vector(fixture.dofs.FullDofCount());
|
||||
return frame;
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidPredictorPreservesRatesFixedDofsAndOldState) {
|
||||
const auto fixture = MakeFixture(4U, true);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
old.velocity[2U] = 0.2;
|
||||
old.velocity[5U] = 0.1;
|
||||
old.acceleration[2U] = 0.3;
|
||||
old.algorithmic_acceleration[2U] = 0.3;
|
||||
old.iteration_count = 7U;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
const auto predictor = fesa::PredictHht(
|
||||
old.generalized_coordinates, old.velocity, old.algorithmic_acceleration,
|
||||
0.1, parameters).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
const auto& frame = start.Value().frame;
|
||||
EXPECT_GT(frame.generalized_coordinates[2U], 0.0);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], predictor.coordinates[i]);
|
||||
EXPECT_DOUBLE_EQ(frame.velocity[i], predictor.velocity[i]);
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.acceleration[i],
|
||||
parameters.alpha * old.acceleration[i] /
|
||||
(1.0 + parameters.alpha));
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
|
||||
}
|
||||
for (const auto i : fixture.dofs.ConstrainedDofs()) {
|
||||
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.velocity[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.acceleration[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(old.velocity[2U], 0.2);
|
||||
EXPECT_DOUBLE_EQ(old.acceleration[2U], 0.3);
|
||||
EXPECT_EQ(old.iteration_count, 7U);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, InvalidPredictorGeometryUsesUnchangedAcceptedCoordinates) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
for (const int invalid_case : {0, 1, 2}) {
|
||||
SCOPED_TRACE(invalid_case);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
if (invalid_case == 0) {
|
||||
predictor.coordinates[5U] = 3.5;
|
||||
} else if (invalid_case == 1) {
|
||||
predictor.coordinates[6U] = -2.0;
|
||||
predictor.coordinates[12U] = -2.0;
|
||||
} else {
|
||||
predictor.coordinates[3U] =
|
||||
std::acos(-1.0) - 2.0 * std::sqrt(std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
|
||||
const double auxiliary =
|
||||
-predictor.coordinates[i] / (parameters.beta * 0.1 * 0.1);
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.algorithmic_acceleration[i], auxiliary);
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.acceleration[i],
|
||||
auxiliary / (1.0 + parameters.alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidChartRateOverflowIsNotReplacedByAcceptedGeometry) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[3U] = 0.1;
|
||||
predictor.velocity[3U] = 1e200;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-dynamic-rotation");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, LaterOutsideChartNodeDoesNotHideEarlierRateOverflow) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[3U] = 0.1;
|
||||
predictor.velocity[3U] = 1e200;
|
||||
predictor.coordinates[11U] = 3.5;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-dynamic-rotation");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, UsesTheExistingPrincipalChartMarginForInitialSelection) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
const double margin = std::sqrt(std::numeric_limits<double>::epsilon());
|
||||
fesa::SerialParallelFor parallel;
|
||||
for (const bool inside : {true, false}) {
|
||||
SCOPED_TRACE(inside);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
const double angle = std::acos(-1.0) - (inside ? 2.0 : 0.5) * margin;
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto& x = fixture.domain->Nodes()[node].coordinates;
|
||||
predictor.coordinates[6U * node] =
|
||||
(std::cos(angle) - 1.0) * x[0U] - std::sin(angle) * x[1U];
|
||||
predictor.coordinates[6U * node + 1U] =
|
||||
std::sin(angle) * x[0U] + (std::cos(angle) - 1.0) * x[1U];
|
||||
predictor.coordinates[6U * node + 5U] = angle;
|
||||
}
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U],
|
||||
inside ? predictor.coordinates[5U] : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, CommonEnergyScalingSelectsTheSmallerFreeResidual) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates[5U] = 0.25;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
0.1, parameters, parallel, false);
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
double drill_residual = 0.0;
|
||||
for (std::size_t i = 3U; i < 24U; i += 6U)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
drill_residual = std::max(
|
||||
drill_residual, std::abs(evaluated.Value().frame.residual[i + j]));
|
||||
ASSERT_GT(drill_residual, 0.0);
|
||||
// A rigid translation gives m*a per node, m=rho*t*area/4=0.2.
|
||||
// The accepted-start force is 0.75 of the predictor torque. Multiplying
|
||||
// force by ell=2 reverses the unscaled force-versus-torque comparison.
|
||||
const double translation =
|
||||
0.75 * drill_residual * (1.0 + parameters.alpha) * parameters.beta *
|
||||
0.1 * 0.1 / 0.2;
|
||||
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
predictor.coordinates[6U * node] = translation;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ConstrainedReactionDoesNotChangeNontrivialFreeResidualTie) {
|
||||
const auto fixture = MakeFixture();
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates[5U] = 0.25;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
0.1, parameters, parallel, false);
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
fesa::Vector load_scale(24U);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
old.external_force[i] = 0.5 * evaluated.Value().frame.internal_force[i];
|
||||
load_scale[i] = std::abs(old.external_force[i]);
|
||||
}
|
||||
// Pure drill spin has zero physical inertia at both starting points, so
|
||||
// the free residuals are exact opposites. A support reaction is excluded.
|
||||
old.external_force[6U] = load_scale[6U] = 1e308;
|
||||
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, load_scale, 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ScaledResidualOverflowRemainsAFailure) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
old.external_force[0U] = 1e308;
|
||||
fesa::Vector load_scale(24U);
|
||||
load_scale[0U] = 1e308;
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[5U] = 0.25;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, load_scale, 0.1,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, AcceptedStartArithmeticFailureIsNotIgnoredDuringComparison) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
predictor.coordinates[6U * node] = 1e10;
|
||||
const double time_increment = 1e-150;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates = predictor.coordinates;
|
||||
const auto valid_predictor = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
time_increment, parameters, parallel, false);
|
||||
ASSERT_TRUE(valid_predictor.HasValue());
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), time_increment, parameters, parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, RejectedPredictorDoesNotHideInvalidAcceptedStartRates) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[5U] = 3.5;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[5U], 0.0);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidGeometryStressOverflowRemainsARecoveryFailure) {
|
||||
const auto fixture = MakeFixture(4U, false, true, 1e307);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t component = 0U; component < 2U; ++component)
|
||||
predictor.coordinates[6U * node + component] =
|
||||
9.0 * fixture.domain->Nodes()[node].coordinates[component];
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-nonlinear-shell-recovery");
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra,
|
||||
@@ -169,6 +492,20 @@ TEST(HhtDynamicAlgebra,
|
||||
EXPECT_FALSE(pseudoinverse.IsOk());
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra, RejectsMissingCanonicalHiddenGradient) {
|
||||
const auto fixture = MakeFixture();
|
||||
auto assembly = Assembly();
|
||||
assembly.hidden_gradient.reset();
|
||||
fesa::MklPardisoSolver positive;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
const auto acceleration = fesa::hht_internal::InitializeAcceleration(
|
||||
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
|
||||
ASSERT_FALSE(acceleration.HasValue());
|
||||
ASSERT_FALSE(acceleration.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(acceleration.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-algebraic-gradient");
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra, ChecksInitialConstraintAndVelocityWithoutRepair) {
|
||||
const auto fixture = MakeFixture();
|
||||
fesa::MklPardisoSolver positive;
|
||||
@@ -240,6 +577,11 @@ TEST(HhtDynamicAlgebra, AppliesTheExactRegularityConditionBoundary) {
|
||||
}
|
||||
assembly.internal_tangent = Sparse(stiffness);
|
||||
assembly.drilling_metric = Sparse(metric);
|
||||
fesa::Matrix hidden_gradient{4U, 24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
hidden_gradient(node, 6U * node + 5U) =
|
||||
node == 1U ? std::ldexp(1.0, -exponent) : 1.0;
|
||||
assembly.hidden_gradient = Sparse(hidden_gradient);
|
||||
const auto result = fesa::hht_internal::InitializeAcceleration(
|
||||
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
|
||||
EXPECT_EQ(result.HasValue(), exponent <= 32)
|
||||
|
||||
@@ -2,15 +2,291 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
namespace {
|
||||
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
|
||||
fesa::Status InjectedTangentFailure() {
|
||||
return fesa::Status::Failure(
|
||||
fesa::FailureCategory::kSolver,
|
||||
{{fesa::Severity::kError, "injected-tangent-backend", {}, "DYNAMIC",
|
||||
{}, "Injected tangent backend failure."}});
|
||||
}
|
||||
|
||||
class FailingTangent final : public fesa::TangentSolver {
|
||||
public:
|
||||
fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override {
|
||||
return InjectedTangentFailure();
|
||||
}
|
||||
fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override {
|
||||
return InjectedTangentFailure();
|
||||
}
|
||||
};
|
||||
|
||||
std::string FailureText(const fesa::Status& status) {
|
||||
std::string result;
|
||||
for (const auto& diagnostic : status.Diagnostics()) {
|
||||
if (!result.empty()) result += " | ";
|
||||
result += diagnostic.code + ": " + diagnostic.message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeDefinition(bool fixed = true);
|
||||
|
||||
fesa::ModelDefinition MakeRigidTranslationDefinition() {
|
||||
auto definition = MakeDefinition(false);
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.nodes.insert(
|
||||
definition.nodes.end(),
|
||||
{{{"Shell-B", 50, "0050"}, {2.0, 0.0, 0.0}, {source, 14U}},
|
||||
{{"Shell-B", 60, "0060"}, {3.0, 0.0, 0.0}, {source, 15U}},
|
||||
{{"Shell-B", 70, "0070"}, {3.0, 1.0, 0.0}, {source, 16U}},
|
||||
{{"Shell-B", 80, "0080"}, {2.0, 1.0, 0.0}, {source, 17U}}});
|
||||
definition.shell_elements.push_back(
|
||||
{{"Shell-B", 19, "0019"}, fesa::ShellSourceElementType::kS4,
|
||||
{4U, 5U, 6U, 7U}, 0U, 0U, {source, 18U}});
|
||||
for (std::size_t node = 4U; node < 8U; ++node) {
|
||||
definition.shell_node_initial_frames.push_back(
|
||||
{static_cast<fesa::EntityIndex>(node), {0.0, 0.0, 1.0},
|
||||
{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}});
|
||||
}
|
||||
definition.node_sets[0U] =
|
||||
{"All-A", std::string{"Shell-A"}, {0U, 1U, 2U, 3U}, {source, 9U}};
|
||||
definition.node_sets.push_back(
|
||||
{"All-B", std::string{"Shell-B"}, {4U, 5U, 6U, 7U}, {source, 19U}});
|
||||
definition.dynamic_step->loads = {{"All-A", 1, 1.0, {source, 20U}},
|
||||
{"All-B", 1, 1.0, {source, 21U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
std::array<double, 3> RotateGlobal(const std::array<double, 3>& value) {
|
||||
constexpr double kCosZ = 0.7648421872844885;
|
||||
constexpr double kSinZ = 0.6442176872376910;
|
||||
constexpr double kCosX = 0.9210609940028851;
|
||||
constexpr double kSinX = 0.3894183423086505;
|
||||
return {kCosZ * value[0U] - kSinZ * kCosX * value[1U] +
|
||||
kSinZ * kSinX * value[2U],
|
||||
kSinZ * value[0U] + kCosZ * kCosX * value[1U] -
|
||||
kCosZ * kSinX * value[2U],
|
||||
kSinX * value[1U] + kCosX * value[2U]};
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeRotatedRigidTranslationDefinition() {
|
||||
auto definition = MakeRigidTranslationDefinition();
|
||||
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
|
||||
for (auto& frame : definition.shell_node_initial_frames) {
|
||||
frame.director = RotateGlobal(frame.director);
|
||||
frame.tangent_a = RotateGlobal(frame.tangent_a);
|
||||
frame.tangent_b = RotateGlobal(frame.tangent_b);
|
||||
}
|
||||
const auto load = RotateGlobal({1.0, 0.0, 0.0});
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.dynamic_step->loads = {{"All-A", 1, load[0U], {source, 22U}},
|
||||
{"All-A", 2, load[1U], {source, 23U}},
|
||||
{"All-A", 3, load[2U], {source, 24U}},
|
||||
{"All-B", 1, load[0U], {source, 25U}},
|
||||
{"All-B", 2, load[1U], {source, 26U}},
|
||||
{"All-B", 3, load[2U], {source, 27U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeFiniteRotationDefinition() {
|
||||
auto definition = MakeDefinition(false);
|
||||
definition.dynamic_step->initial_increment = 0.01;
|
||||
definition.dynamic_step->time_period = 0.02;
|
||||
definition.dynamic_step->increment_count = 2U;
|
||||
definition.dynamic_step->loads = {
|
||||
{"Loaded", 3, 1.0, {"dynamic-state.inp", 23U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeRotatedFiniteRotationDefinition() {
|
||||
auto definition = MakeFiniteRotationDefinition();
|
||||
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
|
||||
for (auto& frame : definition.shell_node_initial_frames) {
|
||||
frame.director = RotateGlobal(frame.director);
|
||||
frame.tangent_a = RotateGlobal(frame.tangent_a);
|
||||
frame.tangent_b = RotateGlobal(frame.tangent_b);
|
||||
}
|
||||
const auto load = RotateGlobal({0.0, 0.0, 1.0});
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.dynamic_step->loads = {{"Loaded", 1, load[0U], {source, 24U}},
|
||||
{"Loaded", 2, load[1U], {source, 25U}},
|
||||
{"Loaded", 3, load[2U], {source, 26U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
struct RigidTranslationRun {
|
||||
fesa::DynamicExecutionResult execution;
|
||||
std::string log;
|
||||
};
|
||||
|
||||
void ExpectSameSourceIds(const std::vector<fesa::SourceEntityId>& left,
|
||||
const std::vector<fesa::SourceEntityId>& right) {
|
||||
ASSERT_EQ(left.size(), right.size());
|
||||
for (std::size_t i = 0U; i < left.size(); ++i) {
|
||||
EXPECT_EQ(left[i].instance_name, right[i].instance_name);
|
||||
EXPECT_EQ(left[i].source_label, right[i].source_label);
|
||||
EXPECT_EQ(left[i].source_label_text, right[i].source_label_text);
|
||||
}
|
||||
}
|
||||
|
||||
fesa::Result<RigidTranslationRun> RunRigidTranslation(
|
||||
const fesa::ModelDefinition& definition, const fesa::ParallelFor& parallel,
|
||||
const std::string& label) {
|
||||
auto domain = fesa::Domain::Create(definition);
|
||||
if (!domain.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
|
||||
domain.GetStatus());
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
const auto path = std::filesystem::temp_directory_path() /
|
||||
("fesa-hht-rigid-" + label + ".log");
|
||||
auto status = log.Open(path);
|
||||
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
|
||||
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value(), log);
|
||||
status = log.Close();
|
||||
std::string log_text;
|
||||
{
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
log_text.assign(std::istreambuf_iterator<char>{input}, {});
|
||||
}
|
||||
std::filesystem::remove(path);
|
||||
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
|
||||
if (!result.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
|
||||
result.GetStatus());
|
||||
return fesa::Result<RigidTranslationRun>::Success(
|
||||
{std::move(result.Value()), log_text});
|
||||
}
|
||||
|
||||
void ExpectSameDynamicHistory(const RigidTranslationRun& left,
|
||||
const RigidTranslationRun& right) {
|
||||
EXPECT_EQ(left.log, right.log);
|
||||
const auto& left_frames = left.execution.state.AcceptedFrames();
|
||||
const auto& right_frames = right.execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(left_frames.size(), right_frames.size());
|
||||
for (std::size_t frame = 0U; frame < left_frames.size(); ++frame) {
|
||||
const auto& lhs = left_frames[frame];
|
||||
const auto& rhs = right_frames[frame];
|
||||
EXPECT_EQ(lhs.time, rhs.time);
|
||||
EXPECT_EQ(lhs.iteration_count, rhs.iteration_count);
|
||||
EXPECT_EQ(lhs.nodal_internal_order, rhs.nodal_internal_order);
|
||||
ExpectSameSourceIds(lhs.nodal_source_ids, rhs.nodal_source_ids);
|
||||
EXPECT_EQ(lhs.nodal_triad, rhs.nodal_triad);
|
||||
EXPECT_EQ(lhs.nodal_director, rhs.nodal_director);
|
||||
for (const auto* values : {&lhs.generalized_coordinates, &lhs.velocity,
|
||||
&lhs.acceleration, &lhs.algorithmic_acceleration,
|
||||
&lhs.inertia_force, &lhs.internal_force,
|
||||
&lhs.external_force, &lhs.residual, &lhs.reaction}) {
|
||||
const auto* other = values == &lhs.generalized_coordinates
|
||||
? &rhs.generalized_coordinates
|
||||
: values == &lhs.velocity ? &rhs.velocity
|
||||
: values == &lhs.acceleration ? &rhs.acceleration
|
||||
: values == &lhs.algorithmic_acceleration
|
||||
? &rhs.algorithmic_acceleration
|
||||
: values == &lhs.inertia_force ? &rhs.inertia_force
|
||||
: values == &lhs.internal_force ? &rhs.internal_force
|
||||
: values == &lhs.external_force ? &rhs.external_force
|
||||
: values == &lhs.residual ? &rhs.residual
|
||||
: &rhs.reaction;
|
||||
ASSERT_EQ(values->Size(), other->Size());
|
||||
for (std::size_t i = 0U; i < values->Size(); ++i)
|
||||
EXPECT_DOUBLE_EQ((*values)[i], (*other)[i]);
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(lhs.kinetic_energy, rhs.kinetic_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.physical_strain_energy, rhs.physical_strain_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.drilling_energy, rhs.drilling_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.external_work, rhs.external_work);
|
||||
EXPECT_DOUBLE_EQ(lhs.energy_defect, rhs.energy_defect);
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectRotatedDofVector(const fesa::Vector& source, const fesa::Vector& target,
|
||||
const double scale) {
|
||||
ASSERT_EQ(source.Size(), target.Size());
|
||||
ASSERT_EQ(source.Size() % 6U, 0U);
|
||||
for (std::size_t node = 0U; node < source.Size() / 6U; ++node)
|
||||
for (const std::size_t offset : {0U, 3U}) {
|
||||
const auto rotated = RotateGlobal(
|
||||
{source[6U * node + offset], source[6U * node + offset + 1U],
|
||||
source[6U * node + offset + 2U]});
|
||||
for (std::size_t component = 0U; component < 3U; ++component)
|
||||
EXPECT_LE(std::abs(rotated[component] -
|
||||
target[6U * node + offset + component]) /
|
||||
scale,
|
||||
1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectCovariantDynamicHistory(const RigidTranslationRun& original,
|
||||
const RigidTranslationRun& rotated,
|
||||
const double time_scale,
|
||||
const double reference_energy) {
|
||||
constexpr double kCoordinateScale = 1.0;
|
||||
const double velocity_scale = kCoordinateScale / time_scale;
|
||||
const double acceleration_scale = kCoordinateScale / (time_scale * time_scale);
|
||||
const auto& original_frames = original.execution.state.AcceptedFrames();
|
||||
const auto& rotated_frames = rotated.execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(original_frames.size(), rotated_frames.size());
|
||||
for (std::size_t frame = 0U; frame < original_frames.size(); ++frame) {
|
||||
const auto& source = original_frames[frame];
|
||||
const auto& target = rotated_frames[frame];
|
||||
ExpectSameSourceIds(source.nodal_source_ids, target.nodal_source_ids);
|
||||
EXPECT_LE(std::abs(source.kinetic_energy - target.kinetic_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(source.physical_strain_energy -
|
||||
target.physical_strain_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(source.drilling_energy - target.drilling_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
ExpectRotatedDofVector(source.generalized_coordinates,
|
||||
target.generalized_coordinates, kCoordinateScale);
|
||||
ExpectRotatedDofVector(source.velocity, target.velocity, velocity_scale);
|
||||
ExpectRotatedDofVector(source.acceleration, target.acceleration,
|
||||
acceleration_scale);
|
||||
ExpectRotatedDofVector(source.algorithmic_acceleration,
|
||||
target.algorithmic_acceleration, acceleration_scale);
|
||||
for (const auto* values : {&source.inertia_force, &source.internal_force,
|
||||
&source.external_force, &source.residual,
|
||||
&source.reaction}) {
|
||||
const auto* other = values == &source.inertia_force ? &target.inertia_force
|
||||
: values == &source.internal_force ? &target.internal_force
|
||||
: values == &source.external_force ? &target.external_force
|
||||
: values == &source.residual ? &target.residual
|
||||
: &target.reaction;
|
||||
ExpectRotatedDofVector(*values, *other, 1.0);
|
||||
}
|
||||
for (std::size_t node = 0U; node < source.nodal_triad.size(); ++node)
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
const auto triad = RotateGlobal(source.nodal_triad[node][row]);
|
||||
const auto director = RotateGlobal(source.nodal_director[node]);
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
EXPECT_LE(std::abs(triad[component] -
|
||||
target.nodal_triad[node][row][component]),
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(director[component] -
|
||||
target.nodal_director[node][component]),
|
||||
1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeDefinition(const bool fixed) {
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = source;
|
||||
@@ -71,6 +347,8 @@ TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
|
||||
: result.GetStatus().Diagnostics().front().message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
|
||||
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
|
||||
@@ -113,6 +391,8 @@ TEST(HhtDynamicProcedure, UnloadedFreeChannelsRemainExactlyAtRest) {
|
||||
: result.GetStatus().Diagnostics().front().message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
for (const auto* values :
|
||||
{&frame.generalized_coordinates, &frame.velocity,
|
||||
@@ -167,7 +447,125 @@ TEST(HhtDynamicProcedure, LargeDirectStepStartsFromAcceptedGeometry) {
|
||||
result.Value().state.CommittedState().free_residual_metrics)
|
||||
EXPECT_LE(metric, 1e-10);
|
||||
EXPECT_GT(result.Value().state.CommittedState().iteration_count, 0U);
|
||||
const auto& frames = result.Value().state.AcceptedFrames();
|
||||
const double alpha = domain.Value().Dynamic()->alpha;
|
||||
for (std::size_t frame_index = 1U; frame_index < frames.size(); ++frame_index) {
|
||||
const auto& frame = frames[frame_index];
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
for (std::size_t dof = 0U; dof < frame.acceleration.Size(); ++dof) {
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof],
|
||||
(1.0 + alpha) * frame.acceleration[dof] -
|
||||
alpha * frames[frame_index - 1U].acceleration[dof]);
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(HhtDynamicProcedure, PreservesBackendCauseAndAddsInitialFailureContext) {
|
||||
auto definition = MakeDefinition(false);
|
||||
definition.dynamic_step->dynamic_location = {"dynamic-state.inp", 13U};
|
||||
auto domain = fesa::Domain::Create(std::move(definition));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
FailingTangent tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
const auto path =
|
||||
std::filesystem::temp_directory_path() / "fesa-hht-context.log";
|
||||
ASSERT_TRUE(log.Open(path).IsOk());
|
||||
const auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value(), log);
|
||||
ASSERT_TRUE(log.Close().IsOk());
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics().front().code,
|
||||
"injected-tangent-backend");
|
||||
const auto found = std::find_if(
|
||||
result.GetStatus().Diagnostics().begin(), result.GetStatus().Diagnostics().end(),
|
||||
[](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.code == "dynamic-procedure-context";
|
||||
});
|
||||
ASSERT_NE(found, result.GetStatus().Diagnostics().end());
|
||||
EXPECT_EQ(found->location.file, std::filesystem::path{"dynamic-state.inp"});
|
||||
EXPECT_EQ(found->location.line, 13U);
|
||||
EXPECT_EQ(found->entity_identity, "Transient Load");
|
||||
EXPECT_NE(found->message.find("stage=initial"), std::string::npos);
|
||||
EXPECT_NE(found->message.find("increment=0"), std::string::npos);
|
||||
EXPECT_NE(found->message.find("time=0"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(HhtDynamicProcedure,
|
||||
UniformTranslationHasNoShellStrainAndIsDeterministicAcrossPartitions) {
|
||||
const auto definition = MakeRigidTranslationDefinition();
|
||||
const auto rotated_definition = MakeRotatedRigidTranslationDefinition();
|
||||
const auto finite_rotation_definition = MakeFiniteRotationDefinition();
|
||||
const auto rotated_finite_rotation_definition =
|
||||
MakeRotatedFiniteRotationDefinition();
|
||||
const fesa::SerialParallelFor serial;
|
||||
const fesa::TbbParallelFor tbb;
|
||||
auto first = RunRigidTranslation(definition, serial, "serial-first");
|
||||
auto partitioned = RunRigidTranslation(definition, tbb, "tbb");
|
||||
auto second = RunRigidTranslation(definition, serial, "serial-second");
|
||||
auto rotated = RunRigidTranslation(rotated_definition, serial, "rotated");
|
||||
auto finite_rotation = RunRigidTranslation(finite_rotation_definition, serial,
|
||||
"finite-rotation");
|
||||
auto rotated_finite_rotation = RunRigidTranslation(
|
||||
rotated_finite_rotation_definition, serial, "rotated-finite-rotation");
|
||||
ASSERT_TRUE(first.HasValue())
|
||||
<< (first.HasValue() ? "" : FailureText(first.GetStatus()));
|
||||
ASSERT_TRUE(partitioned.HasValue())
|
||||
<< (partitioned.HasValue() ? "" : FailureText(partitioned.GetStatus()));
|
||||
ASSERT_TRUE(second.HasValue())
|
||||
<< (second.HasValue() ? "" : FailureText(second.GetStatus()));
|
||||
ASSERT_TRUE(rotated.HasValue())
|
||||
<< (rotated.HasValue() ? "" : FailureText(rotated.GetStatus()));
|
||||
ASSERT_TRUE(finite_rotation.HasValue())
|
||||
<< (finite_rotation.HasValue() ? "" : FailureText(finite_rotation.GetStatus()));
|
||||
ASSERT_TRUE(rotated_finite_rotation.HasValue())
|
||||
<< (rotated_finite_rotation.HasValue()
|
||||
? ""
|
||||
: FailureText(rotated_finite_rotation.GetStatus()));
|
||||
ExpectSameDynamicHistory(first.Value(), partitioned.Value());
|
||||
ExpectSameDynamicHistory(first.Value(), second.Value());
|
||||
ExpectCovariantDynamicHistory(first.Value(), rotated.Value(), 0.5,
|
||||
2.0 * 1000.0 * 0.1);
|
||||
ExpectCovariantDynamicHistory(finite_rotation.Value(),
|
||||
rotated_finite_rotation.Value(), 0.02,
|
||||
1000.0 * 0.1);
|
||||
const auto& frames = first.Value().execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(frames.size(), 3U);
|
||||
constexpr double kReferenceEnergy = 2.0 * 1000.0 * 0.1;
|
||||
constexpr double kCoordinateScale = 1.0;
|
||||
for (const auto& frame : frames) {
|
||||
EXPECT_LE(std::abs(frame.physical_strain_energy) / kReferenceEnergy, 1e-10);
|
||||
EXPECT_LE(std::abs(frame.drilling_energy) / kReferenceEnergy, 1e-10);
|
||||
for (std::size_t node = 1U; node < 8U; ++node)
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node] -
|
||||
frame.generalized_coordinates[0U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
for (std::size_t node = 0U; node < 8U; ++node) {
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 1U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 2U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
for (std::size_t rotation = 3U; rotation < 6U; ++rotation)
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + rotation]),
|
||||
1e-12);
|
||||
}
|
||||
}
|
||||
EXPECT_GT(frames.back().generalized_coordinates[0U], 0.0);
|
||||
double maximum_rotation = 0.0;
|
||||
for (const auto& frame : finite_rotation.Value().execution.state.AcceptedFrames())
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t component = 3U; component < 6U; ++component)
|
||||
maximum_rotation = (std::max)(
|
||||
maximum_rotation, std::abs(frame.generalized_coordinates[6U * node + component]));
|
||||
EXPECT_GT(maximum_rotation, 0.0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -90,7 +90,16 @@ double GradientIntegral(std::size_t a, std::size_t b, std::size_t i,
|
||||
}
|
||||
std::vector<double> DenseOracle(const std::vector<std::size_t>& free) {
|
||||
const auto n = free.size();
|
||||
std::vector<double> k(n * n, 0.0), m(n * n, 0.0), eigen(n, 0.0);
|
||||
std::vector<double> k(n * n, 0.0), full_mass(24U * 24U, 0.0),
|
||||
m(n * n, 0.0), eigen(n, 0.0);
|
||||
for (std::size_t a = 0; a < 8; ++a)
|
||||
for (std::size_t b = 0; b < 8; ++b)
|
||||
for (std::size_t i = 0; i < 3; ++i) {
|
||||
double value = 2.0 * 48.0 / 64.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
value *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
|
||||
full_mass[(3U * a + i) * 24U + 3U * b + i] = value;
|
||||
}
|
||||
for (std::size_t row = 0; row < n; ++row)
|
||||
for (std::size_t col = 0; col < n; ++col) {
|
||||
const auto a = free[row] / 3U, b = free[col] / 3U;
|
||||
@@ -104,12 +113,12 @@ std::vector<double> DenseOracle(const std::vector<std::size_t>& free) {
|
||||
GradientIntegral(a, b, j, i) + (i == j ? gradient_dot : 0.0)) +
|
||||
(80.0 / 48.0) * (48.0 * kSigns[a][i] / (8.0 * (i + 1))) *
|
||||
(48.0 * kSigns[b][j] / (8.0 * (j + 1)));
|
||||
if (i == j) {
|
||||
m[row * n + col] = 2.0 * 48.0 / 64.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
m[row * n + col] *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
|
||||
}
|
||||
}
|
||||
for (std::size_t row = 0; row < n; ++row) {
|
||||
const auto full_row = free[row];
|
||||
for (std::size_t col = 0; col < 24U; ++col)
|
||||
m[row * n + row] += full_mass[full_row * 24U + col];
|
||||
}
|
||||
EXPECT_EQ(
|
||||
LAPACKE_dsygv(LAPACK_ROW_MAJOR, 1, 'N', 'U', static_cast<MKL_INT>(n),
|
||||
k.data(), static_cast<MKL_INT>(n), m.data(),
|
||||
@@ -126,7 +135,7 @@ std::vector<std::size_t> FaceFree() {
|
||||
}
|
||||
} // namespace
|
||||
TEST(SolidFrequencyProcedure,
|
||||
OriginalConsistentPencilMatchesIndependentDenseOracle) {
|
||||
RowSumLumpedPencilMatchesIndependentDenseOracle) {
|
||||
for (const bool constrained : {false, true}) {
|
||||
const std::size_t count = constrained ? 12U : 24U;
|
||||
auto domain = Brick(count, constrained ? kFaceFixed : "");
|
||||
@@ -282,9 +291,9 @@ TEST(SolidFrequencyProcedure,
|
||||
|
||||
TEST(SolidFrequencyProcedure, RejectsOverflowingOriginalPencilValidationScale) {
|
||||
// Every local K entry, eigenvalue, and spectral enclosure is representable.
|
||||
// The all-odd x Walsh mode still overflows the original residual/energy
|
||||
// denominators: finite / infinity must never become passing zero evidence.
|
||||
auto domain = Brick(24U, "", 1.0, 2.0, 3e306);
|
||||
// With M_L=12*I, the highest mode makes ||K||_F+|lambda|*||M||_F
|
||||
// overflow: finite / infinity must never become passing zero evidence.
|
||||
auto domain = Brick(24U, "", 1.0, 2.0, 4e306);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto result =
|
||||
fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
|
||||
@@ -304,7 +304,7 @@ TEST(SolidHhtPairedKinematics, InvalidLowInputsFailWithoutChangingPredictor) {
|
||||
.HasValue());
|
||||
}
|
||||
|
||||
TEST(SolidHhtPairedKinematics, ConsistentMassKeepsSubUlpActionAndCancellation) {
|
||||
TEST(SolidHhtPairedKinematics, MassMultiplyKeepsSubUlpActionAndCancellation) {
|
||||
fesa::SparsePattern pattern;
|
||||
pattern.row_offsets = {0, 2, 4};
|
||||
pattern.column_indices = {0, 1, 0, 1};
|
||||
@@ -569,7 +569,7 @@ TEST(SolidHhtDynamics, RampStartsAtZeroAndAuxiliaryAccelerationIsDistinct) {
|
||||
}
|
||||
|
||||
TEST(SolidHhtDynamics,
|
||||
OneFreeTranslationHasAnalyticConsistentInertialSupportReactions) {
|
||||
OneFreeTranslationHasAnalyticLumpedInertialSupportReactions) {
|
||||
auto result = RunDynamics(OneFree());
|
||||
ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus());
|
||||
const auto d = solid_hht_test::Brick();
|
||||
@@ -581,12 +581,11 @@ TEST(SolidHhtDynamics,
|
||||
const auto& f = frames[n];
|
||||
for (unsigned b = 0; b < 8; ++b) {
|
||||
std::array<double, 3> product{}, sign{};
|
||||
double scalar_mass = 2.0 / 216;
|
||||
double scalar_mass = b == 0 ? .25 : 0.0;
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const bool same = d.nodes[b].coordinates[c] == 0;
|
||||
product[c] = same ? 1.0 / 3 : 1.0 / 6;
|
||||
sign[c] = same ? -1 : 1;
|
||||
scalar_mass *= same ? 2 : 1;
|
||||
}
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
double stiffness;
|
||||
@@ -620,7 +619,7 @@ TEST(SolidHhtDynamics,
|
||||
}
|
||||
if (n != 0) {
|
||||
const auto& old = frames[n - 1];
|
||||
const double mass = 2.0 / 27;
|
||||
const double mass = .25;
|
||||
const double weighted =
|
||||
mass * f.algorithmic_acceleration[0] +
|
||||
.95 * (f.internal_force[0] - f.external_force[0]) +
|
||||
@@ -665,7 +664,7 @@ TEST(SolidHhtDynamics,
|
||||
EXPECT_EQ(state.Value().CommittedState().increment_index, 0U);
|
||||
EXPECT_DOUBLE_EQ(state.Value().CommittedState().displacement[0], 0);
|
||||
EXPECT_NEAR(state.Value().CommittedState().acceleration[0],
|
||||
.01 / (2.0 / 27.0), 1e-14);
|
||||
.01 / .25, 1e-14);
|
||||
ASSERT_FALSE(state.Value().IterationRecords().empty());
|
||||
EXPECT_EQ(state.Value().IterationRecords().back().stage, "failed");
|
||||
EXPECT_FALSE(state.Value().IterationRecords().back().failure_code.empty());
|
||||
|
||||
@@ -79,8 +79,8 @@ fesa::ModelDefinition TwoBricks(double h, unsigned count, bool nonlinear) {
|
||||
fesa::SourceLocation{});
|
||||
return d;
|
||||
}
|
||||
// Closed polynomial brick integrals: integral Na*X and integral grad Na.
|
||||
// These do not call the production stiffness, mass, quadrature or response.
|
||||
// Closed polynomial brick integrals and pre-restriction row-sum masses. These
|
||||
// do not call the production stiffness, mass, quadrature or response.
|
||||
std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
|
||||
bool nonlinear) {
|
||||
const auto m = Motion(t, nonlinear);
|
||||
@@ -89,14 +89,14 @@ std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
|
||||
const auto& nodes = d.solid_elements[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a) {
|
||||
const auto& x = d.nodes[nodes[a]].coordinates;
|
||||
std::array<double, 3> integral_x{}, gradient{};
|
||||
std::array<double, 3> gradient{};
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const double origin = c == 0 ? static_cast<double>(e) : 0;
|
||||
const double local = x[c] - origin;
|
||||
integral_x[c] = 2 * (origin / 8 + (1 + local) / 24);
|
||||
gradient[c] = (2 * local - 1) / 4;
|
||||
}
|
||||
const auto inertia = Apply(m.acceleration, integral_x);
|
||||
const auto inertia = Apply(
|
||||
m.acceleration, {.25 * x[0], .25 * x[1], .25 * x[2]});
|
||||
const auto internal = Apply(m.piola, gradient);
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
f[3 * nodes[a] + c] += inertia[c] + internal[c];
|
||||
@@ -110,16 +110,8 @@ std::vector<double> ExactMassProduct(const fesa::Domain& d,
|
||||
for (unsigned e = 0; e < 2; ++e) {
|
||||
const auto& nodes = d.SolidElements()[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a)
|
||||
for (unsigned b = 0; b < 8; ++b) {
|
||||
double coefficient = 2.0 / 216;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
coefficient *= d.Nodes()[nodes[a]].coordinates[c] ==
|
||||
d.Nodes()[nodes[b]].coordinates[c]
|
||||
? 2
|
||||
: 1;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
result[3 * nodes[a] + c] += coefficient * v[3 * nodes[b] + c];
|
||||
}
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
result[3 * nodes[a] + c] += .25 * v[3 * nodes[a] + c];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -505,8 +497,8 @@ TEST(SolidHhtTemporal,
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
// Node0's three translations are free. Exact polynomial integrals yield
|
||||
// M=(rho/27)I, Kii=41/18, Kij=19/30 for mu=4.8 and direct kappa=8.
|
||||
// Node0's three translations are free. Pre-restriction row-sum lumping
|
||||
// yields M=(rho/8)I, Kii=41/18, Kij=19/30 for mu=4.8 and kappa=8.
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
const double h = .02 / static_cast<double>(1U << level);
|
||||
@@ -528,7 +520,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
ASSERT_TRUE(result.HasValue())
|
||||
<< solid_hht_test::Message(result.GetStatus());
|
||||
const auto& f = result.Value().state.CommittedState();
|
||||
const double m = 2.0 / 27;
|
||||
const double m = .25;
|
||||
const double diagonal = 41.0 / 18;
|
||||
const double off = 19.0 / 30;
|
||||
const std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
|
||||
@@ -562,7 +554,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
const double m = 2.0 / 27;
|
||||
const double m = .25;
|
||||
const double k = 41.0 / 18;
|
||||
const double w = std::sqrt(k / m);
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
|
||||
@@ -395,6 +395,8 @@ TEST(ElementFactory, SolidBindingPreservesThreeComponentsAndReferenceMass) {
|
||||
EXPECT_EQ(stiffness.Value().values.Rows(), 24U);
|
||||
auto mass = solid->ComputeMass();
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
ASSERT_EQ(mass.Value().values.Rows(), 24U);
|
||||
ASSERT_EQ(mass.Value().values.Columns(), 24U);
|
||||
double total_mass = 0.0;
|
||||
for (std::size_t a = 0U; a < 8U; ++a) {
|
||||
for (std::size_t b = 0U; b < 8U; ++b) {
|
||||
@@ -402,7 +404,14 @@ TEST(ElementFactory, SolidBindingPreservesThreeComponentsAndReferenceMass) {
|
||||
}
|
||||
}
|
||||
EXPECT_NEAR(total_mass, 2.0, 1.0e-13);
|
||||
EXPECT_NEAR(mass.Value().values(0U, 3U), 2.0 / 54.0, 1.0e-15);
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
if (row == column)
|
||||
EXPECT_NEAR(mass.Value().values(row, column), 0.25, 1.0e-15);
|
||||
else
|
||||
EXPECT_DOUBLE_EQ(mass.Value().values(row, column), 0.0);
|
||||
}
|
||||
}
|
||||
auto response = solid->EvaluateLinear(fesa::Vector{24U});
|
||||
ASSERT_TRUE(response.HasValue());
|
||||
for (std::size_t point = 0U; point < 8U; ++point) {
|
||||
|
||||
@@ -48,7 +48,7 @@ double GradientProduct(std::size_t a, std::size_t b, std::size_t i,
|
||||
if (d != i && d != j) integral *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
|
||||
return integral;
|
||||
}
|
||||
TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
|
||||
TEST(Hex8Element, ExactBrickStiffnessAndRowSumLumpedMass) {
|
||||
auto element = Make();
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
auto stiffness = element.Value().ComputeStiffness();
|
||||
@@ -61,9 +61,6 @@ TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
|
||||
ASSERT_EQ(m.Columns(), 24U);
|
||||
for (std::size_t a = 0; a < 8; ++a)
|
||||
for (std::size_t b = 0; b < 8; ++b) {
|
||||
double scalar = 2.0 * 48.0 / 216.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
scalar *= kSigns[a][d] == kSigns[b][d] ? 2.0 : 1.0;
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
for (std::size_t j = 0; j < 3; ++j) {
|
||||
double exact = -(2.0 / 3.0) * 48.0 * GradientProduct(a, b, i, j) +
|
||||
@@ -75,7 +72,10 @@ TEST(Hex8Element, ExactBrickStiffnessAndConsistentMass) {
|
||||
exact += (80.0 / 48.0) * (48.0 * kSigns[a][i] / (8.0 * (i + 1))) *
|
||||
(48.0 * kSigns[b][j] / (8.0 * (j + 1)));
|
||||
EXPECT_NEAR(k(3 * a + i, 3 * b + j), exact, 2e-12);
|
||||
EXPECT_NEAR(m(3 * a + i, 3 * b + j), i == j ? scalar : 0.0, 2e-14);
|
||||
// rho*V/8 = 2*48/8. This is the row sum of the complete
|
||||
// consistent mass, before constraints can remove its columns.
|
||||
const double expected_mass = a == b && i == j ? 12.0 : 0.0;
|
||||
EXPECT_NEAR(m(3 * a + i, 3 * b + j), expected_mass, 2e-14);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,24 +351,21 @@ TEST(Hex8Element, SixRigidModesAndZeroStateTangent) {
|
||||
}
|
||||
}
|
||||
TEST(Hex8Element,
|
||||
ConsistentMassHasAll24PositiveDirectionsAndReferenceInvariance) {
|
||||
RowSumLumpedMassHasAll24PositiveDirectionsAndReferenceInvariance) {
|
||||
auto element = Make();
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
auto before = element.Value().ComputeMass().Value().values;
|
||||
// Tensor-product Walsh vectors diagonalize the independent brick scalar
|
||||
// mass; eigenvalues are rho*V/(8*3^number_of_odd_axes), each repeated3.
|
||||
// The complete consistent mass is row-summed before constraints, giving
|
||||
// rho*V/8 on every translational diagonal of this affine brick.
|
||||
for (std::size_t mask = 0; mask < 8; ++mask)
|
||||
for (std::size_t i = 0; i < 3; ++i) {
|
||||
fesa::Vector v{24U};
|
||||
unsigned odd = 0U;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
if ((mask & (1U << d)) != 0U) ++odd;
|
||||
for (std::size_t a = 0; a < 8; ++a) {
|
||||
v[3 * a + i] = 1.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
if ((mask & (1U << d)) != 0U) v[3 * a + i] *= kSigns[a][d];
|
||||
}
|
||||
const double eigenvalue = 12.0 / std::pow(3.0, odd);
|
||||
const double eigenvalue = 12.0;
|
||||
const auto mv = before.Multiply(v);
|
||||
for (std::size_t a = 0; a < 24; ++a)
|
||||
EXPECT_NEAR(mv[a], eigenvalue * v[a], 3e-14);
|
||||
@@ -839,6 +836,27 @@ std::vector<std::array<double, 2>> LegendreRule(unsigned count) {
|
||||
}
|
||||
return points;
|
||||
}
|
||||
std::array<double, 8> WarpedLumpedMass3x3() {
|
||||
const auto rule = LegendreRule(3);
|
||||
std::array<double, 8> mass{};
|
||||
for (const auto& xi : rule)
|
||||
for (const auto& eta : rule)
|
||||
for (const auto& zeta : rule) {
|
||||
const std::array<double, 3> natural{xi[0], eta[0], zeta[0]};
|
||||
// det(dX/d(xi,eta,zeta)) for Warped(), expanded independently from
|
||||
// the production geometry and quadrature paths.
|
||||
const double det =
|
||||
6.0 - .0135 * xi[0] * xi[0] - .036 * eta[0] * eta[0] -
|
||||
.0324 * zeta[0] * zeta[0] + .00324 * xi[0] * eta[0] * zeta[0];
|
||||
for (std::size_t a = 0; a < 8; ++a) {
|
||||
double shape = .125;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
shape *= 1.0 + kSigns[a][d] * natural[d];
|
||||
mass[a] += 2.0 * xi[1] * eta[1] * zeta[1] * shape * det;
|
||||
}
|
||||
}
|
||||
return mass;
|
||||
}
|
||||
Triple Cross(const Triple& a, const Triple& b) {
|
||||
return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0]};
|
||||
@@ -911,6 +929,40 @@ std::array<double, 2> WarpedQuadratureEnergies(const fesa::Vector& u,
|
||||
result[0] += 40.0 * trace_integral * trace_integral / volume;
|
||||
return result;
|
||||
}
|
||||
TEST(Hex8Element,
|
||||
WarpedRowSumMassMatchesIndependentNodalIntegralsAndNonuniformEnergy) {
|
||||
auto element = Make(Warped());
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
const auto mass = element.Value().ComputeMass();
|
||||
ASSERT_TRUE(mass.HasValue());
|
||||
const auto expected = WarpedLumpedMass3x3();
|
||||
const auto& actual = mass.Value().values;
|
||||
double total = 0.0, min_nodal = expected[0], max_nodal = expected[0];
|
||||
fesa::Vector velocity(24);
|
||||
for (std::size_t a = 0; a < 8; ++a) {
|
||||
total += expected[a];
|
||||
min_nodal = (std::min)(min_nodal, expected[a]);
|
||||
max_nodal = (std::max)(max_nodal, expected[a]);
|
||||
velocity[3U * a] = static_cast<double>(a + 1U);
|
||||
for (std::size_t i = 0; i < 3; ++i)
|
||||
for (std::size_t j = 0; j < 3; ++j)
|
||||
EXPECT_NEAR(actual(3U * a + i, 3U * a + j),
|
||||
i == j ? expected[a] : 0.0, 3e-14);
|
||||
}
|
||||
// The xyz term in detJ gives two distinct nodal masses. A uniform rho*V/8
|
||||
// shortcut preserves total mass but fails this kinetic-energy observable.
|
||||
EXPECT_GT(max_nodal - min_nodal, 1e-4);
|
||||
const double lumped_energy = .5 * velocity.Dot(actual.Multiply(velocity));
|
||||
double independent_energy = 0.0, uniform_energy = 0.0;
|
||||
for (std::size_t a = 0; a < 8; ++a) {
|
||||
const double v2 = velocity[3U * a] * velocity[3U * a];
|
||||
independent_energy += .5 * expected[a] * v2;
|
||||
uniform_energy += .5 * total / 8.0 * v2;
|
||||
}
|
||||
EXPECT_NEAR(lumped_energy, independent_energy,
|
||||
3e-14 * independent_energy);
|
||||
EXPECT_GT(std::abs(lumped_energy - uniform_energy), 1e-4);
|
||||
}
|
||||
TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
|
||||
auto element = Make(Warped());
|
||||
ASSERT_TRUE(element.HasValue());
|
||||
@@ -918,26 +970,18 @@ TEST(Hex8Element, WarpedNonaffineQuadratureDifferenceIsQuantifiedAndRefines) {
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
u[i] = .03 * std::sin(.7 * static_cast<double>(i));
|
||||
const auto k = element.Value().ComputeStiffness().Value().values;
|
||||
const auto m = element.Value().ComputeMass().Value().values;
|
||||
const auto q2 = WarpedQuadratureEnergies(u, 2),
|
||||
q3 = WarpedQuadratureEnergies(u, 3);
|
||||
const auto q5 = WarpedQuadratureEnergies(u, 5),
|
||||
q7 = WarpedQuadratureEnergies(u, 7);
|
||||
const double budget = 512.0 * std::numeric_limits<double>::epsilon();
|
||||
EXPECT_NEAR(.5 * u.Dot(k.Multiply(u)), q2[0], budget * q2[0]);
|
||||
EXPECT_NEAR(.5 * u.Dot(m.Multiply(u)), q2[1], budget * q2[1]);
|
||||
const double stiffness_discrepancy = std::abs(q2[0] - q7[0]) / q7[0];
|
||||
const double mass_discrepancy = std::abs(q2[1] - q7[1]) / q7[1];
|
||||
// Mandated2x2 is retained: higher-order integration gives a distinct warped
|
||||
// discretization. Rational stiffness integrands converge with refinement.
|
||||
EXPECT_GT(stiffness_discrepancy, 1e-8);
|
||||
EXPECT_GT(mass_discrepancy, 1e-8);
|
||||
EXPECT_LT(std::abs(q5[0] - q7[0]), .05 * std::abs(q3[0] - q7[0]));
|
||||
// N_a*N_b*detJ has per-axis degree<=4, so order3 integrates mass exactly.
|
||||
EXPECT_NEAR(q3[1], q7[1], budget * q7[1]);
|
||||
EXPECT_NEAR(q5[1], q7[1], budget * q7[1]);
|
||||
std::cout << "warped K2-vs-K7 energy relative=" << stiffness_discrepancy
|
||||
<< " M2-vs-M7 kinetic relative=" << mass_discrepancy
|
||||
<< " K3-vs-K7=" << std::abs(q3[0] - q7[0]) / q7[0]
|
||||
<< " K5-vs-K7=" << std::abs(q5[0] - q7[0]) / q7[0] << '\n';
|
||||
}
|
||||
|
||||
@@ -79,18 +79,18 @@ fesa::Vector Difference(const fesa::Vector& left, const fesa::Vector& right,
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia, IntegratesTheRectangleConsistentMassAndNull) {
|
||||
TEST(NonlinearMitc4Inertia, IntegratesTheRectangleNodalLumpedMassAndNull) {
|
||||
const auto nodes = Nodes();
|
||||
const auto evaluation =
|
||||
Inertia(nodes).Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
|
||||
ASSERT_TRUE(evaluation.HasValue());
|
||||
const fesa::Matrix& mass = evaluation.Value().mass;
|
||||
constexpr std::array<double, 4> kWeights{4.0, 2.0, 1.0, 2.0};
|
||||
constexpr double kC0 = 0.8 / 36.0;
|
||||
for (std::size_t row = 0U; row < 4U; ++row) {
|
||||
for (std::size_t column = 0U; column < 4U; ++column) {
|
||||
const std::size_t offset = (column + 4U - row) % 4U;
|
||||
EXPECT_NEAR(mass(6U * row, 6U * column), kC0 * kWeights[offset], 1.0e-12);
|
||||
EXPECT_NEAR(mass(6U * row, 6U * column), row == column ? 0.2 : 0.0,
|
||||
1.0e-12);
|
||||
EXPECT_NEAR(mass(6U * row + 3U, 6U * column + 3U),
|
||||
row == column ? 0.2 * 0.2 * 0.2 / 12.0 : 0.0, 1.0e-12);
|
||||
}
|
||||
EXPECT_NEAR(mass(6U * row + 5U, 6U * row + 5U), 0.0, 1.0e-12);
|
||||
}
|
||||
@@ -122,6 +122,133 @@ TEST(NonlinearMitc4Inertia, UsesPhysicalAccelerationAndExactCurrentForce) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia,
|
||||
PreservesTinyAngularAccelerationAlongsideRigidTranslationAcceleration) {
|
||||
for (const bool rotated : {false, true}) {
|
||||
SCOPED_TRACE(rotated);
|
||||
auto nodes = Nodes();
|
||||
auto frames = Frames();
|
||||
const auto rotation = fesa::ExpSkew(
|
||||
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
|
||||
const auto transform = [&](const std::array<double, 3>& value) {
|
||||
std::array<double, 3> result{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
result[i] += rotation(i, j) * value[j];
|
||||
return result;
|
||||
};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
nodes[node].coordinates = transform(nodes[node].coordinates);
|
||||
frames[node].director = transform(frames[node].director);
|
||||
frames[node].tangent_a = transform(frames[node].tangent_a);
|
||||
frames[node].tangent_b = transform(frames[node].tangent_b);
|
||||
}
|
||||
const fesa::ShellSection section{"Section", 0.2, 0U, {"tiny.inp", 1U}};
|
||||
const fesa::LinearElasticMaterial material{
|
||||
"Material", 120.0, 0.25, {"tiny.inp", 2U}};
|
||||
const auto inertia = fesa::NonlinearMitc4Inertia::Create(
|
||||
Pointers(nodes), frames, section, material, 1.0);
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
fesa::Vector acceleration{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
acceleration[6U * node + c] = c == 0U ? 15.0 : (c == 1U ? 12.0 : 0.0);
|
||||
acceleration[6U * node + 3U + c] = 1e-17 * frames[node].tangent_a[c];
|
||||
}
|
||||
const auto actual = inertia.Value().Evaluate({}, fesa::Vector{24U}, acceleration);
|
||||
ASSERT_TRUE(actual.HasValue());
|
||||
// Nodal m=rho*A*t/4=.2, rotary inertia=m*t^2/12; c=0 at v=0.
|
||||
constexpr double nodal_rotary_inertia = 0.2 * 0.2 * 0.2 / 12.0;
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
double error = 0.0;
|
||||
double scale = 0.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
const auto full = 6U * node + 3U + c;
|
||||
const double expected = nodal_rotary_inertia * acceleration[full];
|
||||
error = std::max(error, std::abs(actual.Value().force[full] - expected));
|
||||
scale = std::max(scale, std::abs(expected));
|
||||
EXPECT_DOUBLE_EQ(actual.Value().convective_force[full], 0.0);
|
||||
}
|
||||
ASSERT_GT(scale, 0.0);
|
||||
EXPECT_LE(error / scale, 1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At phi=0, Dq(M*a)[tangent_b]=-.5*j*epsilon*director for
|
||||
// a_phi=epsilon*tangent_a. An origin at the active midpoint also isolates
|
||||
// L=j*v_phi and Ldot=j*a_phi from orbital contributions.
|
||||
TEST(NonlinearMitc4Inertia, MixedScaleRotationDerivativeAndAngularMomentum) {
|
||||
for (const bool rotated : {false, true}) {
|
||||
auto nodes = Nodes();
|
||||
auto frames = Frames();
|
||||
const auto rotation = fesa::ExpSkew(
|
||||
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
|
||||
const auto transform = [&](const std::array<double, 3>& value) {
|
||||
std::array<double, 3> result{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
result[i] += rotation(i, j) * value[j];
|
||||
return result;
|
||||
};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
nodes[node].coordinates = transform(nodes[node].coordinates);
|
||||
frames[node].director = transform(frames[node].director);
|
||||
frames[node].tangent_a = transform(frames[node].tangent_a);
|
||||
frames[node].tangent_b = transform(frames[node].tangent_b);
|
||||
}
|
||||
const auto origin = nodes[0].coordinates;
|
||||
for (auto& node : nodes)
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
node.coordinates[c] -= origin[c];
|
||||
const fesa::ShellSection section{"Section", 0.2, 0U, {"tiny.inp", 1U}};
|
||||
const fesa::LinearElasticMaterial material{
|
||||
"Material", 120.0, 0.25, {"tiny.inp", 2U}};
|
||||
const auto inertia = fesa::NonlinearMitc4Inertia::Create(
|
||||
Pointers(nodes), frames, section, material, 1.0);
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
constexpr double nodal_rotary_inertia = 0.2 * 0.2 * 0.2 / 12.0;
|
||||
for (int power = 7; power <= 20; ++power) {
|
||||
SCOPED_TRACE(rotated);
|
||||
SCOPED_TRACE(power);
|
||||
const double epsilon = std::pow(10.0, -power);
|
||||
fesa::Vector acceleration{24U};
|
||||
acceleration[0] = 15.0;
|
||||
acceleration[1] = 12.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
acceleration[3U + c] = epsilon * frames[0].tangent_a[c];
|
||||
const auto actual = inertia.Value().Evaluate(
|
||||
{}, fesa::Vector{24U}, acceleration);
|
||||
ASSERT_TRUE(actual.HasValue());
|
||||
double derivative_error = 0.0;
|
||||
double rate_error = 0.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
double derivative = 0.0;
|
||||
for (std::size_t k = 0U; k < 3U; ++k)
|
||||
derivative += actual.Value().position_tangent(3U + c, 3U + k) *
|
||||
frames[0].tangent_b[k];
|
||||
derivative_error = std::max(derivative_error,
|
||||
std::abs(derivative + 0.5 * nodal_rotary_inertia * epsilon *
|
||||
frames[0].director[c]));
|
||||
rate_error = std::max(rate_error,
|
||||
std::abs(actual.Value().angular_momentum_rate[c] -
|
||||
nodal_rotary_inertia * acceleration[3U + c]));
|
||||
}
|
||||
EXPECT_LE(derivative_error / (nodal_rotary_inertia * epsilon), 1e-12);
|
||||
EXPECT_LE(rate_error / (nodal_rotary_inertia * epsilon), 1e-12);
|
||||
const auto moving = inertia.Value().Evaluate(
|
||||
{}, acceleration, fesa::Vector{24U});
|
||||
ASSERT_TRUE(moving.HasValue());
|
||||
double momentum_error = 0.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
momentum_error = std::max(momentum_error,
|
||||
std::abs(moving.Value().angular_momentum[c] -
|
||||
nodal_rotary_inertia * acceleration[3U + c]));
|
||||
EXPECT_LE(momentum_error / (nodal_rotary_inertia * epsilon), 1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using V3 = fesa::Vector3;
|
||||
|
||||
V3 Multiply(const fesa::Matrix& matrix, const V3& value) {
|
||||
@@ -240,6 +367,48 @@ Fixture Warped() {
|
||||
return fixture;
|
||||
}
|
||||
|
||||
Fixture WarpedNonuniformDirectors() {
|
||||
Fixture fixture = Warped();
|
||||
for (const std::size_t node : {0U, 3U, 1U, 2U}) {
|
||||
const double angle = (node == 0U || node == 3U) ? -0.1 : 0.1;
|
||||
const double cosine = std::cos(angle);
|
||||
const double sine = std::sin(angle);
|
||||
fixture.frames[node].tangent_a = {cosine, 0.0, -sine};
|
||||
fixture.frames[node].tangent_b = {0.0, 1.0, 0.0};
|
||||
fixture.frames[node].director = {sine, 0.0, cosine};
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
V3 RotateFrame(const V3& value) {
|
||||
constexpr double kCosZ = 0.7648421872844885;
|
||||
constexpr double kSinZ = 0.6442176872376910;
|
||||
constexpr double kCosX = 0.9210609940028851;
|
||||
constexpr double kSinX = 0.3894183423086505;
|
||||
return {kCosZ * value.X() - kSinZ * kCosX * value.Y() +
|
||||
kSinZ * kSinX * value.Z(),
|
||||
kSinZ * value.X() + kCosZ * kCosX * value.Y() -
|
||||
kCosZ * kSinX * value.Z(),
|
||||
kSinX * value.Y() + kCosX * value.Z()};
|
||||
}
|
||||
|
||||
Fixture FlatRotatedLargeOffset() {
|
||||
Fixture fixture;
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
fixture.nodes[node].coordinates =
|
||||
(RotateFrame(V3{fixture.nodes[node].coordinates}) +
|
||||
V3{1.0e8, -2.0e8, 3.0e8})
|
||||
.Components();
|
||||
fixture.frames[node].director =
|
||||
RotateFrame(V3{fixture.frames[node].director}).Components();
|
||||
fixture.frames[node].tangent_a =
|
||||
RotateFrame(V3{fixture.frames[node].tangent_a}).Components();
|
||||
fixture.frames[node].tangent_b =
|
||||
RotateFrame(V3{fixture.frames[node].tangent_b}).Components();
|
||||
}
|
||||
return fixture;
|
||||
}
|
||||
|
||||
struct State {
|
||||
fesa::NonlinearMitc4TrialDofs q{};
|
||||
fesa::Vector v{24U};
|
||||
@@ -272,6 +441,44 @@ fesa::Vector ApplyD(fesa::Vector vector, const double length) {
|
||||
return vector;
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia,
|
||||
FlatRotatedLargeOffsetKeepsCenteredRotationalInertiaExactlyZero) {
|
||||
const Fixture fixture = FlatRotatedLargeOffset();
|
||||
const auto inertia = fixture.Create();
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
fesa::Vector acceleration{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
acceleration[6U * node] = 0.3;
|
||||
acceleration[6U * node + 1U] = -0.2;
|
||||
acceleration[6U * node + 2U] = 0.1;
|
||||
}
|
||||
const auto value = inertia.Value().Evaluate({}, fesa::Vector{24U}, acceleration);
|
||||
ASSERT_TRUE(value.HasValue());
|
||||
const auto mass_times_acceleration = value.Value().mass.Multiply(acceleration);
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
EXPECT_DOUBLE_EQ(value.Value().force[6U * node + 3U + component], 0.0);
|
||||
EXPECT_DOUBLE_EQ(mass_times_acceleration[6U * node + 3U + component],
|
||||
0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia, WarpedGeometryRetainsNonzeroFirstThicknessMoment) {
|
||||
const auto inertia = WarpedNonuniformDirectors().Create();
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
const auto value = inertia.Value().Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
|
||||
ASSERT_TRUE(value.HasValue());
|
||||
double coupling = 0.0;
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
if (row % 6U >= 3U && column % 6U < 3U)
|
||||
coupling = std::hypot(coupling, value.Value().mass(row, column));
|
||||
}
|
||||
}
|
||||
EXPECT_GT(coupling, 0.0);
|
||||
}
|
||||
|
||||
fesa::Matrix ScaledMatrix(const fesa::Matrix& matrix, const double length) {
|
||||
fesa::Matrix result{matrix.Rows(), matrix.Columns()};
|
||||
for (std::size_t i = 0; i < matrix.Rows(); ++i) {
|
||||
@@ -459,7 +666,8 @@ struct Integral {
|
||||
V3 angular_momentum_rate{};
|
||||
};
|
||||
|
||||
Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
|
||||
Integral IntegrateFivePoints(const Fixture& fixture, const State& state,
|
||||
const bool lumped = true) {
|
||||
const double r = std::sqrt(10.0 / 7.0);
|
||||
const double outer = std::sqrt(5.0 + 2.0 * r) / 3.0;
|
||||
const double inner = std::sqrt(5.0 - 2.0 * r) / 3.0;
|
||||
@@ -490,63 +698,76 @@ Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
|
||||
for (std::size_t xi = 0; xi < 5; ++xi) {
|
||||
for (std::size_t eta = 0; eta < 5; ++eta) {
|
||||
for (std::size_t zeta = 0; zeta < 5; ++zeta) {
|
||||
const double z = 0.5 * fixture.thickness * points[zeta];
|
||||
std::array<double, 4> shape{};
|
||||
std::array<V3, 24> h{};
|
||||
V3 a1{};
|
||||
V3 a2{};
|
||||
V3 a3{};
|
||||
V3 position{};
|
||||
V3 velocity{};
|
||||
V3 acceleration{};
|
||||
for (std::size_t node = 0; node < 4; ++node) {
|
||||
shape[node] = 0.25 * (1.0 + sx[node] * points[xi]) *
|
||||
(1.0 + sy[node] * points[eta]);
|
||||
const double dx = 0.25 * sx[node] * (1.0 + sy[node] * points[eta]);
|
||||
const double dy = 0.25 * sy[node] * (1.0 + sx[node] * points[xi]);
|
||||
const V3 d0{fixture.frames[node].director};
|
||||
const V3 x0{fixture.nodes[node].coordinates};
|
||||
a1 = a1 + dx * (x0 + z * d0);
|
||||
a2 = a2 + dy * (x0 + z * d0);
|
||||
a3 = a3 + 0.5 * fixture.thickness * shape[node] * d0;
|
||||
position = position + shape[node] * (x0 + state.q.displacement[node] +
|
||||
z * directors[node]);
|
||||
acceleration = acceleration + z * shape[node] * convective[node];
|
||||
for (std::size_t j = 0; j < 3; ++j) {
|
||||
std::array<double, 3> axis{};
|
||||
axis[j] = shape[node];
|
||||
h[6U * node + j] = V3{axis};
|
||||
h[6U * node + 3U + j] = z * shape[node] * first[node][j];
|
||||
for (std::size_t mass_node = 0; mass_node < (lumped ? 4U : 1U);
|
||||
++mass_node) {
|
||||
std::array<double, 4> kinetic_shape{};
|
||||
kinetic_shape[mass_node] = 1.0;
|
||||
const double z = 0.5 * fixture.thickness * points[zeta];
|
||||
std::array<double, 4> shape{};
|
||||
std::array<V3, 24> h{};
|
||||
V3 a1{};
|
||||
V3 a2{};
|
||||
V3 a3{};
|
||||
V3 position{};
|
||||
V3 velocity{};
|
||||
V3 acceleration{};
|
||||
for (std::size_t node = 0; node < 4; ++node) {
|
||||
shape[node] = 0.25 * (1.0 + sx[node] * points[xi]) *
|
||||
(1.0 + sy[node] * points[eta]);
|
||||
if (!lumped) kinetic_shape[node] = shape[node];
|
||||
const double dx = 0.25 * sx[node] * (1.0 + sy[node] * points[eta]);
|
||||
const double dy = 0.25 * sy[node] * (1.0 + sx[node] * points[xi]);
|
||||
const V3 d0{fixture.frames[node].director};
|
||||
const V3 x0{fixture.nodes[node].coordinates};
|
||||
a1 = a1 + dx * (x0 + z * d0);
|
||||
a2 = a2 + dy * (x0 + z * d0);
|
||||
a3 = a3 + 0.5 * fixture.thickness * shape[node] * d0;
|
||||
position = position +
|
||||
kinetic_shape[node] * (x0 + state.q.displacement[node] +
|
||||
z * directors[node]);
|
||||
acceleration =
|
||||
acceleration + z * kinetic_shape[node] * convective[node];
|
||||
for (std::size_t j = 0; j < 3; ++j) {
|
||||
std::array<double, 3> axis{};
|
||||
axis[j] = kinetic_shape[node];
|
||||
h[6U * node + j] = V3{axis};
|
||||
h[6U * node + 3U + j] = z * kinetic_shape[node] * first[node][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
const double weight = fixture.density * weights[xi] * weights[eta] *
|
||||
weights[zeta] * a1.Dot(a2.Cross(a3));
|
||||
EXPECT_GT(weight, 0.0);
|
||||
integral.total_mass += weight;
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
velocity = velocity + h[i] * state.v[i];
|
||||
acceleration = acceleration + h[i] * state.a[i];
|
||||
}
|
||||
integral.energy += 0.5 * weight * velocity.Dot(velocity);
|
||||
integral.power += weight * velocity.Dot(acceleration);
|
||||
integral.momentum = integral.momentum + weight * velocity;
|
||||
integral.angular_momentum =
|
||||
integral.angular_momentum + weight * position.Cross(velocity);
|
||||
integral.momentum_rate = integral.momentum_rate + weight * acceleration;
|
||||
integral.angular_momentum_rate = integral.angular_momentum_rate +
|
||||
weight * position.Cross(acceleration);
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
integral.force[i] += weight * h[i].Dot(acceleration);
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
integral.mass(i, j) += weight * h[i].Dot(h[j]);
|
||||
const double weight = fixture.density * weights[xi] * weights[eta] *
|
||||
weights[zeta] * a1.Dot(a2.Cross(a3)) *
|
||||
(lumped ? shape[mass_node] : 1.0);
|
||||
EXPECT_GT(weight, 0.0);
|
||||
integral.total_mass += weight;
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
velocity = velocity + h[i] * state.v[i];
|
||||
acceleration = acceleration + h[i] * state.a[i];
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
for (std::size_t j = 0; j < 4; ++j) {
|
||||
double moment = weight * shape[i] * shape[j];
|
||||
for (std::size_t k = 0; k < 3; ++k) {
|
||||
integral.moments[k][i][j] += moment;
|
||||
moment *= z;
|
||||
integral.energy += 0.5 * weight * velocity.Dot(velocity);
|
||||
integral.power += weight * velocity.Dot(acceleration);
|
||||
integral.momentum = integral.momentum + weight * velocity;
|
||||
integral.angular_momentum =
|
||||
integral.angular_momentum + weight * position.Cross(velocity);
|
||||
integral.momentum_rate =
|
||||
integral.momentum_rate + weight * acceleration;
|
||||
integral.angular_momentum_rate =
|
||||
integral.angular_momentum_rate +
|
||||
weight * position.Cross(acceleration);
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
integral.force[i] += weight * h[i].Dot(acceleration);
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
integral.mass(i, j) += weight * h[i].Dot(h[j]);
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
for (std::size_t j = 0; j < 4; ++j) {
|
||||
double moment =
|
||||
lumped ? (i == mass_node && j == mass_node ? weight : 0.0)
|
||||
: weight * shape[i] * shape[j];
|
||||
for (std::size_t k = 0; k < 3; ++k) {
|
||||
integral.moments[k][i][j] += moment;
|
||||
moment *= z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,8 +778,9 @@ Integral IntegrateFivePoints(const Fixture& fixture, const State& state) {
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia,
|
||||
MatchesIndependentWarpedVolumeMomentsEnergyAndPower) {
|
||||
for (const Fixture fixture : {Fixture{}, Warped()}) {
|
||||
MatchesIndependentWarpedNodalMomentsEnergyAndPower) {
|
||||
for (const Fixture fixture :
|
||||
{Fixture{}, Warped(), WarpedNonuniformDirectors()}) {
|
||||
const auto inertia = fixture.Create();
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
const State state = FiniteState();
|
||||
@@ -658,6 +880,39 @@ TEST(NonlinearMitc4Inertia,
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Inertia,
|
||||
InitialMassMatchesEigenLumpingAndFiniteEnergyObeysJensen) {
|
||||
for (const Fixture fixture : {Fixture{}, Warped()}) {
|
||||
std::array<std::array<double, 3>, 4> directors{};
|
||||
for (std::size_t node = 0; node < 4; ++node)
|
||||
directors[node] = fixture.frames[node].director;
|
||||
const auto shell = fesa::Mitc4Shell::Create(
|
||||
Pointers(fixture.nodes), directors,
|
||||
{"Section", fixture.thickness, 0U, {"mass-test", 1U}},
|
||||
{"Material", fixture.modulus, 0.25, {"mass-test", 2U}});
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
const auto eigen = shell.Value().Mass(fixture.density);
|
||||
ASSERT_TRUE(eigen.HasValue());
|
||||
const auto inertia = fixture.Create();
|
||||
ASSERT_TRUE(inertia.HasValue());
|
||||
const auto initial =
|
||||
inertia.Value().Evaluate({}, fesa::Vector{24U}, fesa::Vector{24U});
|
||||
ASSERT_TRUE(initial.HasValue());
|
||||
for (std::size_t i = 0; i < 24; ++i) {
|
||||
for (std::size_t j = 0; j < 24; ++j) {
|
||||
EXPECT_NEAR(initial.Value().mass(i, j),
|
||||
eigen.Value().lumped_global24(i, j), 1.0e-12);
|
||||
if (i / 6U != j / 6U) EXPECT_EQ(initial.Value().mass(i, j), 0.0);
|
||||
}
|
||||
}
|
||||
const State state = FiniteState();
|
||||
const auto lumped = IntegrateFivePoints(fixture, state, true);
|
||||
const auto consistent = IntegrateFivePoints(fixture, state, false);
|
||||
EXPECT_GT(lumped.energy - consistent.energy, 0.0);
|
||||
EXPECT_NEAR(lumped.total_mass, consistent.total_mass, 1.0e-12);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DynamicRotation, ThirdDerivativesMatchAllPermutationsAndInteriorSweep) {
|
||||
const std::array<V3, 5> states{
|
||||
V3{}, V3{1.0e-6, -2.0e-6, 3.0e-6}, V3{0.2, -0.3, 0.4},
|
||||
|
||||
@@ -497,9 +497,60 @@ TEST(NonlinearMitc4Residual, ExactDirectionalThirdGradientMatchesTangentSweep) {
|
||||
CheckThirdSweep(Trial());
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, SmallRotationPhysicalForceRetainsDrillingNullIdentity) {
|
||||
// Exact local state of C08b h=1/32768, increment4: the rotation has just
|
||||
// crossed the original 1e-4 coefficient branch and its physical null leaked.
|
||||
auto nodes = Nodes();
|
||||
nodes[1U].coordinates = {0.0, -1.0, 0.0};
|
||||
nodes[2U].coordinates = {0.0, 0.0, 0.0};
|
||||
nodes[3U].coordinates = {-1.0, 0.0, 0.0};
|
||||
const fesa::ShellSection section{"Centered", 0.2, 0U, {}};
|
||||
const fesa::LinearElasticMaterial material{"Elastic", 1000.0, 0.3, {}};
|
||||
const auto shell = fesa::NonlinearMitc4Shell::Create(NodePointers(nodes), Frames(),
|
||||
section, material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
const std::array<std::array<double, 6>, 4> coordinates{{
|
||||
{6.4086914072366947e-05, -7.9345703110834861e-05, -1.4343267109683367e-05, 9.7657633876829583e-05, 7.324020770488069e-05, 4.8828020264083388e-05},
|
||||
{6.4086914054701282e-05, -3.0517578102879528e-05, -9.0026855471989683e-05, 9.7658037630745924e-05, 7.3240207702551634e-05, 4.8828064949797786e-05},
|
||||
{1.5258789062740791e-05, -3.0517578125180581e-05, 7.6293945312499653e-06, 9.7658037632949931e-05, 7.3239803947312715e-05, 4.8828124536813954e-05},
|
||||
{1.5258789080115012e-05, -7.9345703132764625e-05, 8.0871582026927324e-05, 9.7657633879033603e-05, 7.3239803949641825e-05, 4.8828079851099556e-05}
|
||||
}};
|
||||
fesa::NonlinearMitc4TrialDofs trial{};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto& q = coordinates[node];
|
||||
trial.displacement[node] = {q[0U], q[1U], q[2U]};
|
||||
trial.rotation_vector[node] = {q[3U], q[4U], q[5U]};
|
||||
}
|
||||
const fesa::Vector direction(24U);
|
||||
const auto response = fesa::EvaluateInternalResidualAndTangent(shell.Value(), trial, &direction);
|
||||
ASSERT_TRUE(response.HasValue());
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
SCOPED_TRACE(node);
|
||||
const auto rotation = fesa::EvaluateDynamicRotation(trial.rotation_vector[node],
|
||||
{0.0, 0.0, 1.0}, {}, {});
|
||||
ASSERT_TRUE(rotation.HasValue());
|
||||
double physical = 0.0, physical_scale = 0.0, projected = 0.0, total_scale = 0.0;
|
||||
for (std::size_t c = 0U; c < 3U; ++c) {
|
||||
const auto full = 6U * node + 3U + c;
|
||||
const double n = rotation.Value().drill_null[c];
|
||||
physical += n * response.Value().physical_force[full];
|
||||
physical_scale += std::abs(n * response.Value().physical_force[full]);
|
||||
projected += n * response.Value().internal_force[full];
|
||||
total_scale += std::abs(n * response.Value().internal_force[full]);
|
||||
}
|
||||
ASSERT_GT(physical_scale, 0.0);
|
||||
ASSERT_GT(total_scale, 0.0);
|
||||
EXPECT_LE(std::abs(physical) / physical_scale, 1.0e-10);
|
||||
EXPECT_LE(std::abs(projected - response.Value().drilling_constraint[node]) / total_scale,
|
||||
1.0e-10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, ThirdGradientIsStableAcrossSmallAngleBranches) {
|
||||
for (const double angle : {1e-4 * (1 - std::ldexp(1., -10)),
|
||||
1e-4 * (1 + std::ldexp(1., -10)), 2e-4, 1e-3}) {
|
||||
1e-4 * (1 + std::ldexp(1., -10)), 2e-4, 1e-3,
|
||||
.5 * (1 - std::ldexp(1., -12)),
|
||||
.5 * (1 + std::ldexp(1., -12))}) {
|
||||
SCOPED_TRACE(angle);
|
||||
auto trial = Trial();
|
||||
for (auto& phi : trial.rotation_vector) phi = {angle, 0., 0.};
|
||||
@@ -579,4 +630,209 @@ TEST(NonlinearMitc4Residual,
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExposesExactDrillingConstraintThirdDirectionalGradient) {
|
||||
const auto nodes = Nodes();
|
||||
const auto shell = Shell(nodes);
|
||||
const auto trial = Trial();
|
||||
const auto direction = Direction();
|
||||
const auto velocity = ToVector(direction);
|
||||
const auto exact =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell, trial, &velocity);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
constexpr double h = 1.0e-4;
|
||||
const auto plus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, h), &velocity);
|
||||
const auto minus = fesa::EvaluateInternalResidualAndTangent(
|
||||
shell, AddScaled(trial, direction, -h), &velocity);
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
ASSERT_EQ(exact.Value().drilling_constraint_third_directional_gradient[node]
|
||||
.Size(),
|
||||
24U);
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
double plus_hessian_direction = 0.0;
|
||||
double minus_hessian_direction = 0.0;
|
||||
for (std::size_t column = 0U; column < 24U; ++column) {
|
||||
plus_hessian_direction +=
|
||||
plus.Value().drilling_constraint_hessian[node](row, column) *
|
||||
velocity[column];
|
||||
minus_hessian_direction +=
|
||||
minus.Value().drilling_constraint_hessian[node](row, column) *
|
||||
velocity[column];
|
||||
}
|
||||
const double finite_difference =
|
||||
(plus_hessian_direction - minus_hessian_direction) / (2.0 * h);
|
||||
EXPECT_NEAR(
|
||||
exact.Value().drilling_constraint_third_directional_gradient[node]
|
||||
[row],
|
||||
finite_difference, 2.0e-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExternalDrillingConstraintIncludesForceTimeAndCurvatureTerms) {
|
||||
const auto nodes = Nodes();
|
||||
const auto shell = Shell(nodes);
|
||||
const fesa::Vector3 phi{.6, -.2, .1};
|
||||
const fesa::Vector3 phi_dot{.2, -.1, .3};
|
||||
const fesa::Vector3 force{1.7, -.8, .4};
|
||||
const fesa::Vector3 force_rate{-.3, .6, .2};
|
||||
const fesa::Vector3 force_acceleration{.5, -.4, .1};
|
||||
const auto exact = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi, phi_dot, force, force_rate, force_acceleration);
|
||||
ASSERT_TRUE(exact.HasValue());
|
||||
constexpr double h = 1.0e-4;
|
||||
const auto at_time = [&](const double time) {
|
||||
return shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi + time * phi_dot, phi_dot,
|
||||
force + time * force_rate + 0.5 * time * time * force_acceleration,
|
||||
force_rate + time * force_acceleration, force_acceleration);
|
||||
};
|
||||
const auto plus = at_time(h);
|
||||
const auto minus = at_time(-h);
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
EXPECT_NEAR(exact.Value().directional_first,
|
||||
(plus.Value().value - minus.Value().value) / (2.0 * h),
|
||||
2.0e-8);
|
||||
EXPECT_NEAR(exact.Value().directional_second,
|
||||
(plus.Value().value - 2.0 * exact.Value().value +
|
||||
minus.Value().value) /
|
||||
(h * h),
|
||||
3.0e-7);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_NEAR(exact.Value().gradient_directional_first[row],
|
||||
(plus.Value().gradient[row] - minus.Value().gradient[row]) /
|
||||
(2.0 * h),
|
||||
3.0e-8);
|
||||
EXPECT_NEAR(
|
||||
exact.Value().gradient_directional_second[row],
|
||||
(plus.Value().gradient[row] - 2.0 * exact.Value().gradient[row] +
|
||||
minus.Value().gradient[row]) /
|
||||
(h * h),
|
||||
4.0e-7);
|
||||
}
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
fesa::Vector3 basis{};
|
||||
if (column == 0U) basis = {1.0, 0.0, 0.0};
|
||||
if (column == 1U) basis = {0.0, 1.0, 0.0};
|
||||
if (column == 2U) basis = {0.0, 0.0, 1.0};
|
||||
const auto positive = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi + h * basis, phi_dot, force, force_rate, force_acceleration);
|
||||
const auto negative = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, phi - h * basis, phi_dot, force, force_rate, force_acceleration);
|
||||
ASSERT_TRUE(positive.HasValue());
|
||||
ASSERT_TRUE(negative.HasValue());
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_NEAR(exact.Value().hessian(row, column),
|
||||
(positive.Value().gradient[row] -
|
||||
negative.Value().gradient[row]) /
|
||||
(2.0 * h),
|
||||
2.0e-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual,
|
||||
ExternalDrillingConstraintRejectsInvalidRotationAndHasZeroForceValue) {
|
||||
const auto shell = Shell(Nodes());
|
||||
const auto zero = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, {.2, -.1, .3}, {.1, .2, -.3}, {}, {}, {});
|
||||
ASSERT_TRUE(zero.HasValue());
|
||||
EXPECT_DOUBLE_EQ(zero.Value().value, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().directional_first, 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().directional_second, 0.0);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient_directional_first[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(zero.Value().gradient_directional_second[row], 0.0);
|
||||
}
|
||||
const auto invalid = shell.EvaluateExternalDrillingConstraint(
|
||||
0U, {std::acos(-1.0), 0.0, 0.0}, {}, {}, {}, {});
|
||||
EXPECT_FALSE(invalid.HasValue());
|
||||
EXPECT_EQ(invalid.GetStatus().Diagnostics()[0U].code,
|
||||
"invalid-nonlinear-shell-rotation");
|
||||
}
|
||||
|
||||
TEST(NonlinearMitc4Residual, TinyRotationPreservesPhysicalForceAndRecoveryEnergy) {
|
||||
for (const bool rotated : {false, true}) {
|
||||
for (const bool warped : {false, true}) {
|
||||
SCOPED_TRACE(rotated);
|
||||
SCOPED_TRACE(warped);
|
||||
auto nodes = Nodes();
|
||||
if (warped) nodes[2U].coordinates[2U] = 0.1;
|
||||
auto frames = Frames();
|
||||
const auto rotation = fesa::ExpSkew(
|
||||
rotated ? fesa::Vector3{0.4, -0.2, 0.7} : fesa::Vector3{});
|
||||
const auto transform = [&](const Coordinates& values) {
|
||||
Coordinates result{};
|
||||
for (std::size_t i = 0U; i < 3U; ++i)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
result[i] += rotation(i, j) * values[j];
|
||||
return result;
|
||||
};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
nodes[node].coordinates = transform(nodes[node].coordinates);
|
||||
frames[node].director = transform(frames[node].director);
|
||||
frames[node].tangent_a = transform(frames[node].tangent_a);
|
||||
frames[node].tangent_b = transform(frames[node].tangent_b);
|
||||
}
|
||||
const fesa::ShellSection section{"Section", 0.2, 0U, {"tiny.inp", 1U}};
|
||||
const fesa::LinearElasticMaterial material{
|
||||
"Material", 120.0, 0.25, {"tiny.inp", 2U}};
|
||||
const auto shell = fesa::NonlinearMitc4Shell::Create(
|
||||
NodePointers(nodes), frames, section, material);
|
||||
ASSERT_TRUE(shell.HasValue());
|
||||
fesa::NonlinearMitc4TrialDofs tiny{};
|
||||
fesa::Vector increment{24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
tiny.rotation_vector[node] =
|
||||
1e-17 * fesa::Vector3{frames[node].tangent_a};
|
||||
for (std::size_t c = 0U; c < 3U; ++c)
|
||||
increment[6U * node + 3U + c] = tiny.rotation_vector[node][c];
|
||||
}
|
||||
const auto zero =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell.Value(), {});
|
||||
const auto actual =
|
||||
fesa::EvaluateInternalResidualAndTangent(shell.Value(), tiny);
|
||||
ASSERT_TRUE(zero.HasValue());
|
||||
ASSERT_TRUE(actual.HasValue());
|
||||
EXPECT_DOUBLE_EQ(zero.Value().physical_strain_energy, 0.0);
|
||||
double error = 0.0;
|
||||
double scale = 0.0;
|
||||
double quadratic_energy = 0.0;
|
||||
for (std::size_t row = 0U; row < 24U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(zero.Value().physical_force[row], 0.0);
|
||||
double expected = 0.0;
|
||||
for (std::size_t column = 0U; column < 24U; ++column)
|
||||
expected += (zero.Value().internal_tangent(row, column) -
|
||||
zero.Value().drilling_tangent(row, column)) *
|
||||
increment[column];
|
||||
error = std::max(error,
|
||||
std::abs(actual.Value().physical_force[row] - expected));
|
||||
scale = std::max(scale, std::abs(expected));
|
||||
quadratic_energy += 0.5 * increment[row] * expected;
|
||||
}
|
||||
ASSERT_GT(scale, 0.0);
|
||||
ASSERT_GT(quadratic_energy, 0.0);
|
||||
EXPECT_LE(error / scale, 1e-12);
|
||||
EXPECT_GT(actual.Value().physical_strain_energy, 0.0);
|
||||
EXPECT_LE(std::abs(actual.Value().physical_strain_energy - quadratic_energy) /
|
||||
quadratic_energy,
|
||||
1e-12);
|
||||
if (!warped) {
|
||||
// Uniform director tilt has .5 * (5/6) * G * t * area * theta^2.
|
||||
constexpr double shear_energy = 1.6e-33;
|
||||
EXPECT_LE(std::abs(actual.Value().physical_strain_energy - shear_energy) /
|
||||
shear_energy,
|
||||
1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -264,6 +264,42 @@ TEST(DynamicHdf5ResultsWriter, RejectsDefaultStateBeforeCreatingOutput) {
|
||||
EXPECT_FALSE(std::filesystem::exists(output.Path()));
|
||||
}
|
||||
|
||||
TEST(DynamicHdf5ResultsWriter, SeparatesReducedFilterCombinationFromLegacyDriver) {
|
||||
auto fixture = MakeFixture();
|
||||
for (std::uint32_t index = 0U; index <= 2U; ++index) {
|
||||
auto frame = MakeFrame(*fixture.domain, index);
|
||||
frame.time_integration_formulation =
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
|
||||
}
|
||||
TempFile output{"reduced-schema"};
|
||||
fesa::DynamicHdf5ResultsWriter writer;
|
||||
ASSERT_TRUE(writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
|
||||
Hdf5Handle file{
|
||||
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||
H5Fclose};
|
||||
ASSERT_GE(file.Get(), 0);
|
||||
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||
EXPECT_EQ(ReadAttribute<std::uint64_t>(metadata.Get(), "schema_version",
|
||||
H5T_STD_U64LE, H5T_NATIVE_UINT64), 2U);
|
||||
EXPECT_EQ(ReadString(metadata.Get(), "time_integration_formulation"),
|
||||
"reduced-coordinate-HHT-with-physical-rate-reconstruction");
|
||||
for (std::uint32_t index = 0U; index <= 2U; ++index) {
|
||||
const std::string root = "/steps/Step-1/frames/" + std::to_string(index);
|
||||
Hdf5Handle auxiliary{H5Dopen2(file.Get(),
|
||||
(root + "/nodal/algorithmic_acceleration").c_str(),
|
||||
H5P_DEFAULT), H5Dclose};
|
||||
EXPECT_EQ(ReadAttribute<std::uint64_t>(auxiliary.Get(), "schema_version",
|
||||
H5T_STD_U64LE, H5T_NATIVE_UINT64), 2U);
|
||||
EXPECT_EQ(ReadString(auxiliary.Get(), "quantity_semantics"),
|
||||
"full-chart-HHT-filter-combination-not-predictor-driver");
|
||||
Hdf5Handle physical{H5Dopen2(file.Get(), (root + "/nodal/acceleration").c_str(),
|
||||
H5P_DEFAULT), H5Dclose};
|
||||
EXPECT_EQ(ReadString(physical.Get(), "quantity_semantics"),
|
||||
"physical-endpoint-chart-acceleration");
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DynamicHdf5ResultsWriter, WritesCompletePhysicalClampedHistoryAndSchema) {
|
||||
auto fixture = MakeFixture();
|
||||
CommitHistory(fixture);
|
||||
@@ -295,7 +331,7 @@ TEST(DynamicHdf5ResultsWriter, WritesCompletePhysicalClampedHistoryAndSchema) {
|
||||
"hht-implicit-dynamics");
|
||||
EXPECT_EQ(ReadString(metadata.Get(), "source_step_name"), "Transient Load");
|
||||
EXPECT_EQ(ReadString(metadata.Get(), "mass_formulation"),
|
||||
"consistent-kinetic");
|
||||
"nodal-lumped-kinetic");
|
||||
EXPECT_EQ(ReadString(metadata.Get(), "acceleration_convention"),
|
||||
"physical-endpoint-and-auxiliary-separated");
|
||||
EXPECT_EQ(ReadString(metadata.Get(), "initial_state"),
|
||||
|
||||
@@ -217,7 +217,7 @@ TEST(SolidHdf5ResultsWriter,
|
||||
EXPECT_EQ(Attr(metadata.Get(), "source_step_name"), "RawSolid");
|
||||
EXPECT_EQ(Attr(metadata.Get(), "element_formulation"), "FESA-Hex8");
|
||||
EXPECT_EQ(Attr(metadata.Get(), "integration_rule"), "gauss-2x2x2");
|
||||
EXPECT_EQ(Attr(metadata.Get(), "mass_formulation"), "consistent-reference");
|
||||
EXPECT_EQ(Attr(metadata.Get(), "mass_formulation"), "lumped-reference");
|
||||
EXPECT_EQ(Attr(metadata.Get(), "component_order"), "node-major; UX,UY,UZ");
|
||||
Compound(
|
||||
file.Get(), "/model/nodes",
|
||||
|
||||
Reference in New Issue
Block a user