#include "fesa/analysis/analysis.h" #include "fesa/analysis/analysis_procedure.h" #include "fesa/analysis/prepared_analysis.h" #include "analysis/equilibrium_algorithm.h" #include "analysis/hht_reduced_integrator.h" #include "analysis/hht_reduced_procedure.h" #include "analysis/solid_hht_integrator.h" #include "analysis/solid_hht_core.h" #include "analysis/thermal_transient_algebra.h" #include "fesa/solvers/linear/tangent_solver.h" #include #include #include #include #include #include #include namespace { using RunSignature = fesa::Status (fesa::Analysis::*)(const fesa::AnalysisRequest&); static_assert(std::has_virtual_destructor_v); static_assert(std::is_abstract_v); static_assert(std::is_same_v); class StatusAnalysis final : public fesa::Analysis { public: StatusAnalysis(bool& destroyed, fesa::Status result) : destroyed_{destroyed}, result_{std::move(result)} {} ~StatusAnalysis() override { destroyed_ = true; } fesa::Status Run(const fesa::AnalysisRequest& request) override { request_ = request; return result_; } const fesa::AnalysisRequest& Request() const noexcept { return request_; } private: bool& destroyed_; fesa::Status result_; fesa::AnalysisRequest request_; }; } // namespace TEST(Analysis, PropagatesStatusAndDestroysThroughMinimalAnalysis) { bool destroyed = false; const fesa::AnalysisRequest request{"model.inp", "results.h5"}; const fesa::Status failure = fesa::Status::Failure( fesa::FailureCategory::kSolver, {{fesa::Severity::kError, "analysis-test-failure", {}, "ANALYSIS", "", "The concrete procedure failure must propagate unchanged."}}); auto concrete = std::make_unique(destroyed, failure); StatusAnalysis* const concrete_observer = concrete.get(); std::unique_ptr analysis = std::move(concrete); const fesa::Status status = analysis->Run(request); ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, "analysis-test-failure"); EXPECT_EQ(concrete_observer->Request().input_path, request.input_path); EXPECT_EQ(concrete_observer->Request().output_path, request.output_path); analysis.reset(); EXPECT_TRUE(destroyed); } namespace { using ProcedureSignature = fesa::Result ( fesa::AnalysisProcedure::*)(const fesa::Domain &); static_assert(std::has_virtual_destructor_v); static_assert(std::is_abstract_v); static_assert(std::is_same_v); class ObservedProcedure final : public fesa::AnalysisProcedure { public: ObservedProcedure(const fesa::Domain *&observed, bool &destroyed) : observed_(observed), destroyed_(destroyed) {} ~ObservedProcedure() override { destroyed_ = true; } fesa::Result Execute(const fesa::Domain &domain) override { observed_ = &domain; return fesa::Result::Failure( fesa::Status::Failure(fesa::FailureCategory::kSolver, {{fesa::Severity::kError, "procedure-original-failure", {}, "ORIGINAL", "source-identity", "Original failure message."}})); } private: const fesa::Domain *&observed_; bool &destroyed_; }; } // namespace TEST(AnalysisProcedure, PreparedConsumerPreservesDomainFailureAndLifetime) { const auto domain_result = fesa::Domain::Create({}); ASSERT_TRUE(domain_result.HasValue()); const auto &domain = domain_result.Value(); const fesa::Domain *observed = nullptr; bool destroyed = false; std::unique_ptr procedure = std::make_unique(observed, destroyed); auto outcome = fesa::ExecutePreparedAnalysis(domain, *procedure); EXPECT_EQ(observed, &domain); ASSERT_FALSE(outcome.HasValue()); EXPECT_EQ(outcome.GetStatus().Category(), fesa::FailureCategory::kSolver); ASSERT_EQ(outcome.GetStatus().Diagnostics().size(), 1U); const auto &diagnostic = outcome.GetStatus().Diagnostics()[0]; EXPECT_EQ(diagnostic.code, "procedure-original-failure"); EXPECT_EQ(diagnostic.keyword, "ORIGINAL"); EXPECT_EQ(diagnostic.entity_identity, "source-identity"); EXPECT_EQ(diagnostic.message, "Original failure message."); EXPECT_FALSE(destroyed); procedure.reset(); EXPECT_TRUE(destroyed); } TEST(EquilibriumAlgorithm, RetainsMoveOnlyCandidateUntilProcedureTakesIt) { static_assert(std::has_virtual_destructor_v); static_assert(std::is_abstract_v); using Candidate = std::unique_ptr; using EvaluationResult = fesa::Result>; std::vector observed; fesa::EquilibriumStep step( [&](std::uint32_t iteration) { observed.push_back(iteration); if (iteration == 3U) return EvaluationResult::Success(std::make_unique(42)); return EvaluationResult::Success(std::nullopt); }, 100U); fesa::NewtonStepAlgorithm algorithm; fesa::EquilibriumAlgorithm& base = algorithm; EXPECT_FALSE(step.HasCandidate()); ASSERT_TRUE(step.Solve(base).IsOk()); EXPECT_EQ(observed, (std::vector{1U, 2U, 3U})); ASSERT_TRUE(step.HasCandidate()); Candidate accepted = step.TakeCandidate(); ASSERT_NE(accepted, nullptr); EXPECT_EQ(*accepted, 42); } TEST(EquilibriumAlgorithm, FirstFailedEvaluationLeavesNoAcceptedCandidate) { using EvaluationResult = fesa::Result>; std::vector observed; fesa::EquilibriumStep step( [&](std::uint32_t iteration) { observed.push_back(iteration); return EvaluationResult::Failure(fesa::Status::Failure( fesa::FailureCategory::kSolver, {{fesa::Severity::kError, "first-evaluation-failure", {}, "STATIC", "original-owner", "Original evaluation failure."}})); }, 100U); fesa::NewtonStepAlgorithm algorithm; fesa::EquilibriumAlgorithm& base = algorithm; const auto status = step.Solve(base); EXPECT_EQ(observed, (std::vector{1U})); EXPECT_FALSE(step.HasCandidate()); ASSERT_FALSE(status.IsOk()); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0].code, "first-evaluation-failure"); EXPECT_EQ(status.Diagnostics()[0].entity_identity, "original-owner"); EXPECT_EQ(status.Diagnostics()[0].message, "Original evaluation failure."); } namespace { // These helpers route supplied values only. They never call a concrete time // integrator, Domain/assembly/time-grid builder, analysis or numerical solver. fesa::Status InjectionFailure() { return fesa::Status::Failure(fesa::FailureCategory::kSolver, {{fesa::Severity::kError, "injected-policy-failure", {}, "MOCK", "caller-owned", "Original injected failure."}}); } void ExpectInjectionFailure(const fesa::Status& status) { ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kSolver); ASSERT_EQ(status.Diagnostics().size(), 1U); const auto& diagnostic = status.Diagnostics()[0]; EXPECT_EQ(diagnostic.code, "injected-policy-failure"); EXPECT_EQ(diagnostic.keyword, "MOCK"); EXPECT_EQ(diagnostic.entity_identity, "caller-owned"); EXPECT_EQ(diagnostic.message, "Original injected failure."); } void ExpectValueVector(const fesa::Vector& value, double expected) { ASSERT_EQ(value.Size(), 2U); for (std::size_t i = 0; i < value.Size(); ++i) EXPECT_DOUBLE_EQ(value[i], expected); } class NeverCalledLinear final : public fesa::LinearSolver { public: fesa::Status Factorize(const fesa::SparseMatrix&) override { ADD_FAILURE() << "Mock dispatch must not factorize."; return InjectionFailure(); } fesa::Status Solve(const fesa::Vector&, fesa::Vector&) const override { ADD_FAILURE() << "Mock dispatch must not solve."; return InjectionFailure(); } }; class NeverCalledTangent final : public fesa::TangentSolver { public: fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override { ADD_FAILURE() << "Mock dispatch must not factorize."; return InjectionFailure(); } fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override { ADD_FAILURE() << "Mock dispatch must not solve."; return InjectionFailure(); } }; class SelectedOrderAlgorithm final : public fesa::EquilibriumAlgorithm { public: explicit SelectedOrderAlgorithm(bool& destroyed) : destroyed_(destroyed) {} ~SelectedOrderAlgorithm() override { destroyed_ = true; } bool fail = false; fesa::Status SolveCurrentStep(std::uint32_t first, std::uint32_t last, const Evaluation& evaluate) override { EXPECT_EQ(first, 0U); EXPECT_EQ(last, 3U); if (fail) return InjectionFailure(); auto first_result = evaluate(first); if (!first_result.HasValue()) return first_result.GetStatus(); EXPECT_FALSE(first_result.Value()); auto last_result = evaluate(last); if (!last_result.HasValue()) return last_result.GetStatus(); EXPECT_TRUE(last_result.Value()); return fesa::Status::Ok(); } private: bool& destroyed_; }; namespace shell_value = fesa::hht_reduced_internal; class ValueShellIntegrator final : public shell_value::ShellHhtIntegrator { public: ValueShellIntegrator(bool& destroyed, std::vector& events) : destroyed_(destroyed), events_(events) {} ~ValueShellIntegrator() override { destroyed_ = true; } std::function predict; std::function rates; std::function( const shell_value::FixedChart&, const shell_value::EmbeddingRoot&, const fesa::Vector&, const fesa::Vector&, fesa::TangentSolver&)> reconstruct; shell_value::ReducedShellPredictor Predict( const shell_value::ReducedHistory& history, double h, const fesa::HhtParameters& parameters) const override { events_.push_back("predict"); return predict(history, h, parameters); } void EvaluateRates(const shell_value::ReducedShellRateRequest& request, fesa::Vector& velocity, fesa::Vector& acceleration) const override { events_.push_back("rates"); rates(request, velocity, acceleration); } fesa::Result ReconstructPhysicalRates( const shell_value::FixedChart& chart, const shell_value::EmbeddingRoot& root, const fesa::Vector& velocity, const fesa::Vector& acceleration, fesa::TangentSolver& solver) const override { events_.push_back("reconstruct"); return reconstruct(chart, root, velocity, acceleration, solver); } private: bool& destroyed_; std::vector& events_; }; class ValueSolidIntegrator final : public fesa::detail::SolidHhtIntegrator { public: ValueSolidIntegrator(bool& destroyed, std::vector& events) : destroyed_(destroyed), events_(events) {} ~ValueSolidIntegrator() override { destroyed_ = true; } std::function( const fesa::SolidDynamicAcceptedFrameState&, double, const fesa::HhtParameters&)> predict; std::function( const fesa::Vector&, const fesa::Vector&, const fesa::detail::SolidHhtPairedPredictor&, const fesa::Vector&, const fesa::Vector&, double, const fesa::HhtParameters&)> rates; fesa::Result Predict( const fesa::SolidDynamicAcceptedFrameState& old, double h, const fesa::HhtParameters& parameters) const override { events_.push_back("predict"); return predict(old, h, parameters); } fesa::Result EvaluateRates( const fesa::Vector& high, const fesa::Vector& low, const fesa::detail::SolidHhtPairedPredictor& predictor, const fesa::Vector& old_acceleration, const fesa::Vector& old_acceleration_low, double h, const fesa::HhtParameters& parameters) const override { events_.push_back("rates"); return rates(high, low, predictor, old_acceleration, old_acceleration_low, h, parameters); } private: bool& destroyed_; std::vector& events_; }; namespace heat_value = fesa::thermal_transient_internal; class ValueHeatIntegrator final : public heat_value::HeatStepIntegrator { public: ValueHeatIntegrator(bool& destroyed, std::vector& events) : destroyed_(destroyed), events_(events) {} ~ValueHeatIntegrator() override { destroyed_ = true; } std::function( const heat_value::ThermalSystem&, const heat_value::ThermalPartition&, const fesa::Vector&, double, fesa::LinearSolver&, heat_value::ThermalFactorization&)> solve; fesa::Result SolveIncrement( const heat_value::ThermalSystem& system, const heat_value::ThermalPartition& partition, const fesa::Vector& old, double delta, fesa::LinearSolver& solver, heat_value::ThermalFactorization& cache) const override { events_.push_back("solve"); return solve(system, partition, old, delta, solver, cache); } private: bool& destroyed_; std::vector& events_; }; } // namespace TEST(EquilibriumAlgorithm, InjectsAlternativeOrderAndPreservesTypedPendingOnFailure) { bool destroyed = false; auto owned = std::make_unique(destroyed); auto* selected = owned.get(); std::unique_ptr policy = std::move(owned); std::vector observed; using Candidate = std::unique_ptr; using EvaluationResult = fesa::Result>; fesa::EquilibriumStep step( [&](std::uint32_t iteration) { observed.push_back(iteration); return iteration == 3U ? EvaluationResult::Success(std::make_unique(42)) : EvaluationResult::Success(std::nullopt); }, 3U); ASSERT_TRUE(step.Solve(*policy).IsOk()); EXPECT_EQ(observed, (std::vector{0U, 3U})); selected->fail = true; ExpectInjectionFailure(step.Solve(*policy)); EXPECT_EQ(observed, (std::vector{0U, 3U})); ASSERT_TRUE(step.HasCandidate()); EXPECT_EQ(*step.TakeCandidate(), 42); EXPECT_FALSE(destroyed); policy.reset(); EXPECT_TRUE(destroyed); // The default algorithm still evaluates zero through the inclusive maximum. std::vector zero_observed; fesa::NewtonStepAlgorithm zero_algorithm; fesa::EquilibriumStep zero_step([&](std::uint32_t iteration) { zero_observed.push_back(iteration); return iteration == 2U ? fesa::Result>::Success(7) : fesa::Result>::Success(std::nullopt); }, 2U); ASSERT_TRUE(zero_step.Solve(zero_algorithm).IsOk()); EXPECT_EQ(zero_observed, (std::vector{0U, 1U, 2U})); ASSERT_TRUE(zero_step.HasCandidate()); EXPECT_EQ(zero_step.TakeCandidate(), 7); } TEST(ProcedureIntegratorInjection, ShellValuesUseProductionStepAndPreserveHistory) { bool destroyed = false; std::vector events; auto owned = std::make_unique(destroyed, events); auto* fake = owned.get(); std::unique_ptr integrator = std::move(owned); shell_value::ReducedHistory old{fesa::Vector(2, 1), fesa::Vector(2, 2), fesa::Vector(2, 3), fesa::Vector(2, 4), fesa::Vector(2, 5)}; shell_value::FixedChart chart; shell_value::EmbeddingRoot root; fesa::Vector algorithmic(2, 6), retained_v(2, 7), retained_a(2, 8); const fesa::HhtParameters parameters{11, 12, 13}; NeverCalledTangent solver; bool fail_reconstruction = false, fail_commit = false; fake->predict = [&](const auto& history, double h, const auto& p) { EXPECT_EQ(&history, &old); EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); return shell_value::ReducedShellPredictor{fesa::Vector(2, 21), fesa::Vector(2, 22), fesa::Vector(2, 23), fesa::Vector(2, 24), fesa::Vector(2, 25), fesa::Vector(2, 26)}; }; fake->rates = [&](const auto& request, auto& velocity, auto& acceleration) { EXPECT_EQ(&request.history, &old); EXPECT_EQ(&request.chart, &chart); EXPECT_EQ(&request.trial_algorithmic, &algorithmic); EXPECT_EQ(&request.retained_velocity, &retained_v); EXPECT_EQ(&request.retained_acceleration, &retained_a); EXPECT_EQ(&request.parameters, ¶meters); EXPECT_DOUBLE_EQ(request.h, 17); EXPECT_FALSE(request.copy_translation_rates); ExpectValueVector(request.vpp, 22); EXPECT_NE(velocity.Data(), acceleration.Data()); EXPECT_NE(velocity.Data(), request.vpp.Data()); EXPECT_NE(acceleration.Data(), request.trial_algorithmic.Data()); ExpectValueVector(velocity, 0); ExpectValueVector(acceleration, 0); for (std::size_t i = 0; i < 2; ++i) { velocity[i] = 31; acceleration[i] = 32; } }; fake->reconstruct = [&](const auto& c, const auto& r, const auto& v, const auto& a, auto& backend) { EXPECT_EQ(&c, &chart); EXPECT_EQ(&r, &root); EXPECT_EQ(&backend, &solver); ExpectValueVector(v, 31); ExpectValueVector(a, 32); if (fail_reconstruction) return fesa::Result::Failure(InjectionFailure()); return fesa::Result::Success({ fesa::Vector(2, 41), fesa::Vector(2, 42), fesa::Vector(2, 43), fesa::Vector(2, 44)}); }; std::vector history; const auto evaluate = [&](const auto& selected, auto predictor) { ExpectValueVector(predictor.pp, 21); ExpectValueVector(predictor.vpp, 22); ExpectValueVector(predictor.coordinate_low, 23); ExpectValueVector(predictor.increment_trace, 24); ExpectValueVector(predictor.increment_trace_compensation, 25); ExpectValueVector(predictor.increment_trace_absolute_sum, 26); shell_value::ReducedShellTrialRates rates{fesa::Vector(2), fesa::Vector(2)}; auto* v_data = rates.velocity.Data(); auto* a_data = rates.acceleration.Data(); selected.EvaluateRates({chart, old, algorithmic, predictor.vpp, false, retained_v, retained_a, 17, parameters}, rates.velocity, rates.acceleration); EXPECT_EQ(v_data, rates.velocity.Data()); EXPECT_EQ(a_data, rates.acceleration.Data()); return selected.ReconstructPhysicalRates(chart, root, rates.velocity, rates.acceleration, solver); }; const auto commit = [&](shell_value::ReconstructedRates&& pending) { events.push_back("commit"); ExpectValueVector(pending.velocity, 41); ExpectValueVector(pending.acceleration, 42); ExpectValueVector(pending.z_velocity, 43); ExpectValueVector(pending.z_acceleration, 44); if (fail_commit) return InjectionFailure(); history.push_back(std::move(pending)); return fesa::Status::Ok(); }; ASSERT_TRUE(shell_value::RunReducedHhtStep(*integrator, old, 17, parameters, evaluate, commit).IsOk()); EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct", "commit"})); ASSERT_EQ(history.size(), 1U); events.clear(); fail_reconstruction = true; ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17, parameters, evaluate, commit)); EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct"})); EXPECT_EQ(history.size(), 1U); events.clear(); fail_reconstruction = false; fail_commit = true; ExpectInjectionFailure(shell_value::RunReducedHhtStep(*integrator, old, 17, parameters, evaluate, commit)); EXPECT_EQ(events, (std::vector{"predict", "rates", "reconstruct", "commit"})); EXPECT_EQ(history.size(), 1U); ExpectValueVector(old.p, 1); ExpectValueVector(old.velocity, 2); ExpectValueVector(old.acceleration, 3); ExpectValueVector(old.algorithmic_acceleration, 4); ExpectValueVector(old.z, 5); ExpectValueVector(history[0].velocity, 41); EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); } TEST(ProcedureIntegratorInjection, SolidPairedValuesUseProductionStepAndPreserveHistory) { bool destroyed = false; std::vector events; auto owned = std::make_unique(destroyed, events); auto* fake = owned.get(); std::unique_ptr integrator = std::move(owned); fesa::SolidDynamicAcceptedFrameState old; old.displacement = fesa::Vector(2, 1); old.low.displacement = fesa::Vector(2, 2); old.velocity = fesa::Vector(2, 3); old.low.velocity = fesa::Vector(2, 4); old.acceleration = fesa::Vector(2, 5); old.low.acceleration = fesa::Vector(2, 6); old.algorithmic_acceleration = fesa::Vector(2, 7); old.low.algorithmic_acceleration = fesa::Vector(2, 8); const fesa::HhtParameters parameters{11, 12, 13}; bool fail_predict = false, fail_rates = false, fail_commit = false; fake->predict = [&](const auto& frame, double h, const auto& p) { EXPECT_EQ(&frame, &old); EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); if (fail_predict) return fesa::Result::Failure(InjectionFailure()); return fesa::Result::Success({ fesa::Vector(2, 21), fesa::Vector(2, 22), fesa::Vector(2, 23), fesa::Vector(2, 24)}); }; fake->rates = [&](const auto& high, const auto& low, const auto& predictor, const auto& old_a, const auto& old_a_low, double h, const auto& p) { ExpectValueVector(high, 1); ExpectValueVector(low, 2); ExpectValueVector(predictor.displacement, 21); ExpectValueVector(predictor.displacement_low, 22); ExpectValueVector(predictor.velocity, 23); ExpectValueVector(predictor.velocity_low, 24); EXPECT_EQ(&old_a, &old.acceleration); EXPECT_EQ(&old_a_low, &old.low.acceleration); EXPECT_EQ(&p, ¶meters); EXPECT_DOUBLE_EQ(h, 17); if (fail_rates) return fesa::Result::Failure(InjectionFailure()); return fesa::Result::Success({ fesa::Vector(2, 31), fesa::Vector(2, 32), fesa::Vector(2, 33), fesa::Vector(2, 34), fesa::Vector(2, 35), fesa::Vector(2, 36)}); }; std::vector history; const auto failure = [&](const fesa::Status& status) { events.push_back("predict-failure"); return status; }; const auto evaluate = [&](const auto& selected, const auto& predictor) { auto pending = old; const auto status = fesa::detail::SetSolidHhtRates(selected, old, predictor, 17, parameters, &pending); if (!status.IsOk()) { ExpectValueVector(pending.velocity, 3); ExpectValueVector(pending.low.velocity, 4); ExpectValueVector(pending.acceleration, 5); ExpectValueVector(pending.low.acceleration, 6); ExpectValueVector(pending.algorithmic_acceleration, 7); ExpectValueVector(pending.low.algorithmic_acceleration, 8); return fesa::Result::Failure(status); } return fesa::Result::Success(std::move(pending)); }; const auto commit = [&](fesa::SolidDynamicAcceptedFrameState&& pending) { events.push_back("commit"); ExpectValueVector(pending.displacement, 1); ExpectValueVector(pending.low.displacement, 2); ExpectValueVector(pending.velocity, 31); ExpectValueVector(pending.low.velocity, 32); ExpectValueVector(pending.acceleration, 33); ExpectValueVector(pending.low.acceleration, 34); ExpectValueVector(pending.algorithmic_acceleration, 35); ExpectValueVector(pending.low.algorithmic_acceleration, 36); if (fail_commit) return InjectionFailure(); history.push_back(std::move(pending)); return fesa::Status::Ok(); }; ASSERT_TRUE(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters, failure, evaluate, commit).IsOk()); EXPECT_EQ(events, (std::vector{"predict", "rates", "commit"})); ASSERT_EQ(history.size(), 1U); events.clear(); fail_predict = true; ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters, failure, evaluate, commit)); EXPECT_EQ(events, (std::vector{"predict", "predict-failure"})); EXPECT_EQ(history.size(), 1U); events.clear(); fail_predict = false; fail_rates = true; ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters, failure, evaluate, commit)); EXPECT_EQ(events, (std::vector{"predict", "rates"})); EXPECT_EQ(history.size(), 1U); events.clear(); fail_rates = false; fail_commit = true; ExpectInjectionFailure(fesa::detail::RunSolidHhtStep(*integrator, old, 17, parameters, failure, evaluate, commit)); EXPECT_EQ(events, (std::vector{"predict", "rates", "commit"})); EXPECT_EQ(history.size(), 1U); ExpectValueVector(old.displacement, 1); ExpectValueVector(old.low.displacement, 2); ExpectValueVector(old.velocity, 3); ExpectValueVector(old.low.velocity, 4); ExpectValueVector(old.acceleration, 5); ExpectValueVector(old.low.acceleration, 6); ExpectValueVector(old.algorithmic_acceleration, 7); ExpectValueVector(old.low.algorithmic_acceleration, 8); ExpectValueVector(history[0].low.algorithmic_acceleration, 36); EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); } TEST(ProcedureIntegratorInjection, HeatValuesUseProductionRecoveryCommitAndPreserveHistory) { bool destroyed = false; std::vector events; auto owned = std::make_unique(destroyed, events); auto* fake = owned.get(); std::unique_ptr integrator = std::move(owned); auto matrix = fesa::SparseMatrix::FromCoo( 2, 2, {}, fesa::SparsePattern{{0, 0, 0}, {}}); ASSERT_TRUE(matrix.HasValue()); const auto& k = matrix.Value(); const fesa::SparseMatrix c{k}; const fesa::Vector q(2, 2), old(2, 1); const heat_value::ThermalSystem system{k, c, q}; const heat_value::ThermalPartition partition{{0, 1}, {}, fesa::Vector(0)}; heat_value::ThermalFactorization cache; NeverCalledLinear solver; bool fail_solve = false, fail_recovery = false, fail_commit = false; fake->solve = [&](const auto& supplied, const auto& p, const auto& previous, double delta, auto& backend, auto& workspace) { EXPECT_EQ(&supplied, &system); EXPECT_EQ(&p, &partition); EXPECT_EQ(&previous, &old); EXPECT_EQ(&backend, &solver); EXPECT_EQ(&workspace, &cache); EXPECT_DOUBLE_EQ(delta, 17); EXPECT_EQ(&supplied.k, &k); EXPECT_EQ(&supplied.c, &c); EXPECT_EQ(&supplied.q, &q); EXPECT_EQ(supplied.k_error, nullptr); EXPECT_EQ(supplied.c_error, nullptr); EXPECT_EQ(supplied.q_error, nullptr); EXPECT_EQ(p.free, (std::vector{0, 1})); EXPECT_TRUE(p.constrained.empty()); EXPECT_EQ(p.prescribed.Size(), 0U); if (fail_solve) return fesa::Result::Failure(InjectionFailure()); heat_value::ThermalStepCandidate pending{fesa::Vector(2, 21), fesa::Vector(2, 22), fesa::Vector(2, 23), fesa::Vector(2, 24), {31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, true, 42, 43}}; return fesa::Result::Success(std::move(pending)); }; const heat_value::ThermalRecovery recovery = [&](const fesa::Vector& temperature) { events.push_back("recovery"); ExpectValueVector(temperature, 21); if (fail_recovery) return fesa::Result>::Failure(InjectionFailure()); std::vector flux(1); for (auto& value : flux[0]) value = fesa::Vector3{51, 52, 53}; return fesa::Result>::Success(std::move(flux)); }; std::vector history; const auto commit = [&](heat_value::ThermalStepCandidate&& pending, std::vector&& flux) { events.push_back("commit"); ExpectValueVector(pending.temperature, 21); ExpectValueVector(pending.residual, 22); ExpectValueVector(pending.reaction, 23); ExpectValueVector(pending.certified_residual, 24); const auto& e = pending.evidence; EXPECT_DOUBLE_EQ(e.beta_cert, 31); EXPECT_DOUBLE_EQ(e.beta_limit, 32); EXPECT_DOUBLE_EQ(e.free_residual_cert, 33); EXPECT_DOUBLE_EQ(e.full_free_residual_cert, 34); EXPECT_DOUBLE_EQ(e.full_beta_cert, 35); EXPECT_DOUBLE_EQ(e.operator_construction_error, 36); EXPECT_DOUBLE_EQ(e.rhs_construction_error, 37); EXPECT_DOUBLE_EQ(e.construction_identity_bound, 38); EXPECT_DOUBLE_EQ(e.energy_change, 39); EXPECT_DOUBLE_EQ(e.energy_defect, 40); EXPECT_DOUBLE_EQ(e.energy_bound, 41); EXPECT_TRUE(e.dissipation_checked); EXPECT_DOUBLE_EQ(e.dissipation_defect, 42); EXPECT_DOUBLE_EQ(e.dissipation_bound, 43); EXPECT_EQ(flux.size(), 1U); for (const auto& value : flux[0]) EXPECT_EQ(value.Components(), (std::array{51, 52, 53})); if (fail_commit) return InjectionFailure(); history.push_back(std::move(pending)); return fesa::Status::Ok(); }; ASSERT_TRUE(heat_value::RunThermalStep(*integrator, system, partition, old, 17, solver, cache, recovery, commit).IsOk()); EXPECT_EQ(events, (std::vector{"solve", "recovery", "commit"})); ASSERT_EQ(history.size(), 1U); events.clear(); fail_solve = true; ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, old, 17, solver, cache, recovery, commit)); EXPECT_EQ(events, (std::vector{"solve"})); EXPECT_EQ(history.size(), 1U); events.clear(); fail_solve = false; fail_recovery = true; ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, old, 17, solver, cache, recovery, commit)); EXPECT_EQ(events, (std::vector{"solve", "recovery"})); EXPECT_EQ(history.size(), 1U); events.clear(); fail_recovery = false; fail_commit = true; ExpectInjectionFailure(heat_value::RunThermalStep(*integrator, system, partition, old, 17, solver, cache, recovery, commit)); EXPECT_EQ(events, (std::vector{"solve", "recovery", "commit"})); EXPECT_EQ(history.size(), 1U); ExpectValueVector(old, 1); ExpectValueVector(history[0].temperature, 21); EXPECT_FALSE(cache.matrix.has_value()); EXPECT_FALSE(destroyed); integrator.reset(); EXPECT_TRUE(destroyed); }