feat(nonlinear-static-newton-raphson): step 10 - prepared-analysis-flow-and-full-procedure
This commit is contained in:
@@ -1,18 +1,7 @@
|
|||||||
#ifndef FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_
|
#ifndef FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_
|
||||||
#define FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_
|
#define FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "fesa/analysis/analysis.h"
|
#include "fesa/analysis/analysis.h"
|
||||||
#include "fesa/analysis/analysis_model.h"
|
|
||||||
#include "fesa/analysis/analysis_state.h"
|
|
||||||
#include "fesa/constraints/essential_constraint_policy.h"
|
|
||||||
#include "fesa/elements/element.h"
|
|
||||||
#include "fesa/fem/dof_manager.h"
|
|
||||||
#include "fesa/math/sparse_matrix.h"
|
|
||||||
#include "fesa/math/vector.h"
|
|
||||||
#include "fesa/model/domain.h"
|
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
@@ -20,9 +9,8 @@ class LinearSolver;
|
|||||||
class ParallelFor;
|
class ParallelFor;
|
||||||
class ResultsWriter;
|
class ResultsWriter;
|
||||||
|
|
||||||
/// @brief Orchestrates the single-step linear-static procedure.
|
/// @brief Retains the legacy Analysis facade for linear-static callers.
|
||||||
/// @note Factorization, substitution, recovery, and writing remain separately
|
/// @note Numerical stages are owned by LinearStaticProcedureExecutor.
|
||||||
/// observable through injected backend boundaries.
|
|
||||||
class LinearStaticAnalysis final : public Analysis {
|
class LinearStaticAnalysis final : public Analysis {
|
||||||
public:
|
public:
|
||||||
/// @brief Creates a procedure using non-owned backend adapters.
|
/// @brief Creates a procedure using non-owned backend adapters.
|
||||||
@@ -31,42 +19,14 @@ class LinearStaticAnalysis final : public Analysis {
|
|||||||
LinearSolver& linear_solver,
|
LinearSolver& linear_solver,
|
||||||
ResultsWriter& results_writer);
|
ResultsWriter& results_writer);
|
||||||
|
|
||||||
/// @brief Runs the approved eight-stage linear-static lifecycle.
|
/// @brief Prepares, executes, and writes a linear-static analysis.
|
||||||
/// @return The first stage failure or successful result finalization.
|
/// @return The first stage failure or successful result finalization.
|
||||||
Status Run(const AnalysisRequest& request) override;
|
Status Run(const AnalysisRequest& request) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// @brief Initializes owned input and Domain state for a run candidate.
|
|
||||||
Status InitializeCandidate(const AnalysisRequest& request);
|
|
||||||
/// @brief Builds the non-owning active-model view.
|
|
||||||
Status BuildAnalysisModel();
|
|
||||||
/// @brief Creates runtime elements, stable DOFs, and the sparse pattern.
|
|
||||||
Status BuildDofMapAndSparsePattern();
|
|
||||||
/// @brief Assembles full stiffness and stable constraint partitions.
|
|
||||||
Status AssembleAndPartitionStiffness();
|
|
||||||
/// @brief Factorizes Kff before any load assembly.
|
|
||||||
Status FactorizeFreeSystem();
|
|
||||||
/// @brief Assembles loads and forms Ff-Kfc*dc without solving.
|
|
||||||
Status AssembleLoadsAndEffectiveRhs();
|
|
||||||
/// @brief Substitutes the retained factorization and reconstructs full d.
|
|
||||||
Status SubstituteAndReconstruct();
|
|
||||||
/// @brief Recovers a complete candidate before writing authoritative output.
|
|
||||||
Status RecoverAndWrite();
|
|
||||||
|
|
||||||
const ParallelFor& parallel_for_;
|
const ParallelFor& parallel_for_;
|
||||||
LinearSolver& linear_solver_;
|
LinearSolver& linear_solver_;
|
||||||
ResultsWriter& results_writer_;
|
ResultsWriter& results_writer_;
|
||||||
AnalysisRequest request_;
|
|
||||||
std::unique_ptr<Domain> domain_;
|
|
||||||
std::unique_ptr<AnalysisModel> model_;
|
|
||||||
std::vector<std::unique_ptr<Element>> elements_;
|
|
||||||
ElementView element_view_;
|
|
||||||
std::unique_ptr<DofManager> dofs_;
|
|
||||||
std::unique_ptr<AnalysisState> state_;
|
|
||||||
std::unique_ptr<SparseMatrix> full_stiffness_;
|
|
||||||
std::unique_ptr<PartitionedStiffness> partitioned_stiffness_;
|
|
||||||
std::unique_ptr<Vector> effective_rhs_;
|
|
||||||
std::vector<Diagnostic> diagnostics_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#ifndef FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_
|
||||||
|
#define FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_
|
||||||
|
|
||||||
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
class LinearSolver;
|
||||||
|
class ParallelFor;
|
||||||
|
|
||||||
|
/// @brief Executes the approved linear-static procedure on a prepared Domain.
|
||||||
|
class LinearStaticProcedureExecutor {
|
||||||
|
public:
|
||||||
|
/// @brief Creates a procedure executor with caller-owned backend adapters.
|
||||||
|
/// @param parallel_for Deterministic element-local execution adapter.
|
||||||
|
/// @param linear_solver Factorization and substitution adapter.
|
||||||
|
LinearStaticProcedureExecutor(const ParallelFor& parallel_for,
|
||||||
|
LinearSolver& linear_solver);
|
||||||
|
|
||||||
|
/// @brief Computes linear-static state without writing HDF5 output.
|
||||||
|
/// @return A validated state and sorted diagnostics, or a procedure failure.
|
||||||
|
Result<LinearStaticExecutionResult> Execute(const Domain& domain) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const ParallelFor& parallel_for_;
|
||||||
|
LinearSolver& linear_solver_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_ANALYSIS_LINEAR_STATIC_PROCEDURE_H_
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#ifndef FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_
|
||||||
|
#define FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_
|
||||||
|
|
||||||
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
class NewtonIterationLogWriter;
|
||||||
|
class ParallelFor;
|
||||||
|
class TangentSolver;
|
||||||
|
|
||||||
|
/// @brief Executes fixed-load-factor full-Newton nonlinear static analysis.
|
||||||
|
class NonlinearStaticProcedureExecutor {
|
||||||
|
public:
|
||||||
|
/// @brief Creates a procedure executor with caller-owned backend adapters.
|
||||||
|
/// @param parallel_for Deterministic element-local execution adapter.
|
||||||
|
/// @param tangent_solver Fresh unsymmetric tangent solve adapter.
|
||||||
|
NonlinearStaticProcedureExecutor(const ParallelFor& parallel_for,
|
||||||
|
TangentSolver& tangent_solver);
|
||||||
|
|
||||||
|
/// @brief Computes accepted nonlinear frames and appends iteration records.
|
||||||
|
/// @return A validated nonlinear state without writing HDF5 output.
|
||||||
|
Result<NonlinearStaticExecutionResult> Execute(
|
||||||
|
const Domain& domain, NewtonIterationLogWriter& log) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const ParallelFor& parallel_for_;
|
||||||
|
TangentSolver& tangent_solver_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_ANALYSIS_NONLINEAR_STATIC_PROCEDURE_H_
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#ifndef FESA_ANALYSIS_PREPARED_ANALYSIS_H_
|
||||||
|
#define FESA_ANALYSIS_PREPARED_ANALYSIS_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_state.h"
|
||||||
|
#include "fesa/analysis/nonlinear_analysis_state.h"
|
||||||
|
#include "fesa/core/diagnostic.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
class LinearStaticProcedureExecutor;
|
||||||
|
class NewtonIterationLogWriter;
|
||||||
|
class NonlinearResultsWriter;
|
||||||
|
class NonlinearStaticProcedureExecutor;
|
||||||
|
class ResultsWriter;
|
||||||
|
struct DerivedOutputPaths;
|
||||||
|
|
||||||
|
/// @brief Owns one parsed and semantically mapped analysis input.
|
||||||
|
struct PreparedAnalysisInput {
|
||||||
|
Domain domain;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Parses and maps an input file exactly once for procedure execution.
|
||||||
|
/// @param input_path Source Abaqus input path.
|
||||||
|
/// @return A movable Domain and sorted mapping diagnostics, or an input
|
||||||
|
/// failure.
|
||||||
|
Result<PreparedAnalysisInput> PrepareAnalysisInput(
|
||||||
|
const std::filesystem::path& input_path);
|
||||||
|
|
||||||
|
/// @brief Stores a completed linear-static calculation before result writing.
|
||||||
|
struct LinearStaticExecutionResult {
|
||||||
|
AnalysisState state;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Stores a completed nonlinear-static calculation before result
|
||||||
|
/// writing.
|
||||||
|
struct NonlinearStaticExecutionResult {
|
||||||
|
NonlinearAnalysisState state;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
using PreparedAnalysisOutcome =
|
||||||
|
std::variant<LinearStaticExecutionResult, NonlinearStaticExecutionResult>;
|
||||||
|
|
||||||
|
/// @brief Selects the approved procedure from a prepared Domain.
|
||||||
|
/// @pre The executors and log outlive this call.
|
||||||
|
/// @return A validated procedure outcome without opening a result file.
|
||||||
|
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||||
|
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||||
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
|
NewtonIterationLogWriter& log);
|
||||||
|
|
||||||
|
/// @brief Writes an already computed outcome to its authoritative result file.
|
||||||
|
/// @pre The Newton log has closed successfully before this function is called.
|
||||||
|
/// @return Success after the selected writer atomically finalizes its result.
|
||||||
|
Status FinalizePreparedAnalysisResult(
|
||||||
|
const DerivedOutputPaths& paths, const Domain& domain,
|
||||||
|
const PreparedAnalysisOutcome& outcome,
|
||||||
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
|
NonlinearResultsWriter& nonlinear_writer);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_ANALYSIS_PREPARED_ANALYSIS_H_
|
||||||
@@ -4,9 +4,12 @@ add_library(
|
|||||||
analysis/analysis_model.cpp
|
analysis/analysis_model.cpp
|
||||||
analysis/analysis_state.cpp
|
analysis/analysis_state.cpp
|
||||||
analysis/linear_static_analysis.cpp
|
analysis/linear_static_analysis.cpp
|
||||||
|
analysis/linear_static_procedure.cpp
|
||||||
analysis/newton_iteration_log.cpp
|
analysis/newton_iteration_log.cpp
|
||||||
analysis/nonlinear_analysis_state.cpp
|
analysis/nonlinear_analysis_state.cpp
|
||||||
analysis/nonlinear_static_control.cpp
|
analysis/nonlinear_static_control.cpp
|
||||||
|
analysis/nonlinear_static_procedure.cpp
|
||||||
|
analysis/prepared_analysis.cpp
|
||||||
app/fesa_application.cpp
|
app/fesa_application.cpp
|
||||||
app/output_paths.cpp
|
app/output_paths.cpp
|
||||||
assembly/load_assembler.cpp
|
assembly/load_assembler.cpp
|
||||||
|
|||||||
@@ -2,50 +2,12 @@
|
|||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "fesa/assembly/load_assembler.h"
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
#include "fesa/assembly/parallel_for.h"
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
#include "fesa/assembly/sparse_assembler.h"
|
|
||||||
#include "fesa/elements/element_factory.h"
|
|
||||||
#include "fesa/io/abaqus/domain_mapper.h"
|
|
||||||
#include "fesa/io/abaqus/input_reader.h"
|
|
||||||
#include "fesa/results/result_recovery.h"
|
|
||||||
#include "fesa/results/results_writer.h"
|
#include "fesa/results/results_writer.h"
|
||||||
#include "fesa/solvers/linear/linear_solver.h"
|
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
Status LinearStaticAnalysis::Run(const AnalysisRequest& request) {
|
|
||||||
Status status = InitializeCandidate(request);
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = BuildAnalysisModel();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = BuildDofMapAndSparsePattern();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = AssembleAndPartitionStiffness();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = FactorizeFreeSystem();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = AssembleLoadsAndEffectiveRhs();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
status = SubstituteAndReconstruct();
|
|
||||||
if (!status.IsOk()) {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
return RecoverAndWrite();
|
|
||||||
}
|
|
||||||
|
|
||||||
LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for,
|
LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for,
|
||||||
LinearSolver& linear_solver,
|
LinearSolver& linear_solver,
|
||||||
ResultsWriter& results_writer)
|
ResultsWriter& results_writer)
|
||||||
@@ -53,138 +15,23 @@ LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for,
|
|||||||
linear_solver_{linear_solver},
|
linear_solver_{linear_solver},
|
||||||
results_writer_{results_writer} {}
|
results_writer_{results_writer} {}
|
||||||
|
|
||||||
Status LinearStaticAnalysis::InitializeCandidate(
|
Status LinearStaticAnalysis::Run(const AnalysisRequest& request) {
|
||||||
const AnalysisRequest& request) {
|
Result<PreparedAnalysisInput> prepared =
|
||||||
// Clear dependent objects in reverse ownership order so a reused analysis
|
PrepareAnalysisInput(request.input_path);
|
||||||
// never exposes a view into a Domain from an earlier run.
|
if (!prepared.HasValue()) {
|
||||||
effective_rhs_.reset();
|
return prepared.GetStatus();
|
||||||
partitioned_stiffness_.reset();
|
|
||||||
full_stiffness_.reset();
|
|
||||||
state_.reset();
|
|
||||||
dofs_.reset();
|
|
||||||
element_view_.clear();
|
|
||||||
elements_.clear();
|
|
||||||
model_.reset();
|
|
||||||
domain_.reset();
|
|
||||||
diagnostics_.clear();
|
|
||||||
request_ = request;
|
|
||||||
|
|
||||||
const auto parsed = AbaqusInputReader{}.Read(request_.input_path);
|
|
||||||
if (!parsed.HasValue()) {
|
|
||||||
return parsed.GetStatus();
|
|
||||||
}
|
|
||||||
auto domain = AbaqusDomainMapper{}.Map(parsed.Value());
|
|
||||||
if (!domain.HasValue()) {
|
|
||||||
return domain.GetStatus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
domain_ = std::make_unique<Domain>(std::move(domain.Value()));
|
LinearStaticProcedureExecutor executor(parallel_for_, linear_solver_);
|
||||||
diagnostics_ = domain_->Warnings();
|
Result<LinearStaticExecutionResult> execution =
|
||||||
SortDiagnostics(diagnostics_);
|
executor.Execute(prepared.Value().domain);
|
||||||
return Status::Ok();
|
if (!execution.HasValue()) {
|
||||||
}
|
return execution.GetStatus();
|
||||||
|
|
||||||
Status LinearStaticAnalysis::BuildAnalysisModel() {
|
|
||||||
auto model = AnalysisModel::Create(*domain_);
|
|
||||||
if (!model.HasValue()) {
|
|
||||||
return model.GetStatus();
|
|
||||||
}
|
|
||||||
model_ = std::make_unique<AnalysisModel>(std::move(model.Value()));
|
|
||||||
return Status::Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::BuildDofMapAndSparsePattern() {
|
|
||||||
std::vector<std::unique_ptr<Element>> elements;
|
|
||||||
ElementView element_view;
|
|
||||||
elements.reserve(model_->ActiveElements().size());
|
|
||||||
element_view.reserve(model_->ActiveElements().size());
|
|
||||||
const ElementFactory factory;
|
|
||||||
for (const EntityIndex element_index : model_->ActiveElements()) {
|
|
||||||
auto element = factory.Create(domain_->Elements()[element_index], *domain_);
|
|
||||||
if (!element.HasValue()) {
|
|
||||||
return element.GetStatus();
|
|
||||||
}
|
|
||||||
element_view.emplace_back(*element.Value());
|
|
||||||
elements.push_back(std::move(element.Value()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DofManager dofs;
|
return results_writer_.Write(request.output_path, prepared.Value().domain,
|
||||||
const Status dof_status = dofs.Build(*model_, element_view);
|
execution.Value().state,
|
||||||
if (!dof_status.IsOk()) {
|
execution.Value().diagnostics);
|
||||||
return dof_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
elements_ = std::move(elements);
|
|
||||||
element_view_ = std::move(element_view);
|
|
||||||
dofs_ = std::make_unique<DofManager>(std::move(dofs));
|
|
||||||
state_ = std::make_unique<AnalysisState>(
|
|
||||||
AnalysisState::Create(*dofs_, {"Step-1", 0U}));
|
|
||||||
return Status::Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::AssembleAndPartitionStiffness() {
|
|
||||||
auto stiffness =
|
|
||||||
SparseAssembler::Assemble(element_view_, *dofs_, parallel_for_);
|
|
||||||
if (!stiffness.HasValue()) {
|
|
||||||
return stiffness.GetStatus();
|
|
||||||
}
|
|
||||||
full_stiffness_ =
|
|
||||||
std::make_unique<SparseMatrix>(std::move(stiffness.Value()));
|
|
||||||
|
|
||||||
auto partitioned =
|
|
||||||
EssentialConstraintPolicy{}.Partition(*full_stiffness_, *dofs_);
|
|
||||||
if (!partitioned.HasValue()) {
|
|
||||||
return partitioned.GetStatus();
|
|
||||||
}
|
|
||||||
partitioned_stiffness_ =
|
|
||||||
std::make_unique<PartitionedStiffness>(std::move(partitioned.Value()));
|
|
||||||
return Status::Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::FactorizeFreeSystem() {
|
|
||||||
// This call intentionally precedes all load assembly in Run.
|
|
||||||
return linear_solver_.Factorize(partitioned_stiffness_->k_ff);
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::AssembleLoadsAndEffectiveRhs() {
|
|
||||||
auto full_load = LoadAssembler::AssembleFullNodalLoad(*model_, *dofs_);
|
|
||||||
if (!full_load.HasValue()) {
|
|
||||||
return full_load.GetStatus();
|
|
||||||
}
|
|
||||||
state_->ExternalForce() = std::move(full_load.Value());
|
|
||||||
|
|
||||||
auto rhs = LoadAssembler::EffectiveFreeRhs(state_->ExternalForce(),
|
|
||||||
partitioned_stiffness_->k_fc,
|
|
||||||
dofs_->PrescribedValues(), *dofs_);
|
|
||||||
if (!rhs.HasValue()) {
|
|
||||||
return rhs.GetStatus();
|
|
||||||
}
|
|
||||||
effective_rhs_ = std::make_unique<Vector>(std::move(rhs.Value()));
|
|
||||||
return Status::Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::SubstituteAndReconstruct() {
|
|
||||||
Vector free_displacement{dofs_->FreeDofCount()};
|
|
||||||
const Status solve_status =
|
|
||||||
linear_solver_.Solve(*effective_rhs_, free_displacement);
|
|
||||||
if (!solve_status.IsOk()) {
|
|
||||||
return solve_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
state_->Displacement() = EssentialConstraintPolicy{}.ReconstructFull(
|
|
||||||
free_displacement, dofs_->PrescribedValues(), *dofs_);
|
|
||||||
return Status::Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Status LinearStaticAnalysis::RecoverAndWrite() {
|
|
||||||
const Status recovery_status = ResultRecovery::Recover(
|
|
||||||
*model_, element_view_, *dofs_, *full_stiffness_, state_->Displacement(),
|
|
||||||
state_->ExternalForce(), *state_);
|
|
||||||
if (!recovery_status.IsOk()) {
|
|
||||||
return recovery_status;
|
|
||||||
}
|
|
||||||
return results_writer_.Write(request_.output_path, *domain_, *state_,
|
|
||||||
diagnostics_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/assembly/load_assembler.h"
|
||||||
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/assembly/sparse_assembler.h"
|
||||||
|
#include "fesa/constraints/essential_constraint_policy.h"
|
||||||
|
#include "fesa/elements/element.h"
|
||||||
|
#include "fesa/elements/element_factory.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/results/result_recovery.h"
|
||||||
|
#include "fesa/solvers/linear/linear_solver.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
LinearStaticProcedureExecutor::LinearStaticProcedureExecutor(
|
||||||
|
const ParallelFor& parallel_for, LinearSolver& linear_solver)
|
||||||
|
: parallel_for_(parallel_for), linear_solver_(linear_solver) {}
|
||||||
|
|
||||||
|
Result<LinearStaticExecutionResult> LinearStaticProcedureExecutor::Execute(
|
||||||
|
const Domain& domain) const {
|
||||||
|
Result<AnalysisModel> model = AnalysisModel::Create(domain);
|
||||||
|
if (!model.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(model.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<Element>> elements;
|
||||||
|
ElementView element_view;
|
||||||
|
elements.reserve(model.Value().ActiveElements().size());
|
||||||
|
element_view.reserve(model.Value().ActiveElements().size());
|
||||||
|
const ElementFactory factory;
|
||||||
|
for (const EntityIndex element_index : model.Value().ActiveElements()) {
|
||||||
|
Result<std::unique_ptr<Element>> element =
|
||||||
|
factory.Create(domain.Elements()[element_index], domain);
|
||||||
|
if (!element.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(element.GetStatus());
|
||||||
|
}
|
||||||
|
element_view.emplace_back(*element.Value());
|
||||||
|
elements.push_back(std::move(element.Value()));
|
||||||
|
}
|
||||||
|
|
||||||
|
DofManager dofs;
|
||||||
|
const Status dof_status = dofs.Build(model.Value(), element_view);
|
||||||
|
if (!dof_status.IsOk()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(dof_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalysisState state = AnalysisState::Create(dofs, {"Step-1", 0U});
|
||||||
|
Result<SparseMatrix> stiffness =
|
||||||
|
SparseAssembler::Assemble(element_view, dofs, parallel_for_);
|
||||||
|
if (!stiffness.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(stiffness.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<PartitionedStiffness> partitions =
|
||||||
|
EssentialConstraintPolicy{}.Partition(stiffness.Value(), dofs);
|
||||||
|
if (!partitions.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(partitions.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
const Status factorized = linear_solver_.Factorize(partitions.Value().k_ff);
|
||||||
|
if (!factorized.IsOk()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(factorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Vector> external =
|
||||||
|
LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs);
|
||||||
|
if (!external.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(external.GetStatus());
|
||||||
|
}
|
||||||
|
state.ExternalForce() = std::move(external.Value());
|
||||||
|
|
||||||
|
Result<Vector> rhs = LoadAssembler::EffectiveFreeRhs(
|
||||||
|
state.ExternalForce(), partitions.Value().k_fc, dofs.PrescribedValues(),
|
||||||
|
dofs);
|
||||||
|
if (!rhs.HasValue()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(rhs.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector free_displacement{dofs.FreeDofCount()};
|
||||||
|
const Status solved = linear_solver_.Solve(rhs.Value(), free_displacement);
|
||||||
|
if (!solved.IsOk()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(solved);
|
||||||
|
}
|
||||||
|
state.Displacement() = EssentialConstraintPolicy{}.ReconstructFull(
|
||||||
|
free_displacement, dofs.PrescribedValues(), dofs);
|
||||||
|
|
||||||
|
const Status recovered = ResultRecovery::Recover(
|
||||||
|
model.Value(), element_view, dofs, stiffness.Value(),
|
||||||
|
state.Displacement(), state.ExternalForce(), state);
|
||||||
|
if (!recovered.IsOk()) {
|
||||||
|
return Result<LinearStaticExecutionResult>::Failure(recovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Diagnostic> diagnostics = domain.Warnings();
|
||||||
|
SortDiagnostics(diagnostics);
|
||||||
|
return Result<LinearStaticExecutionResult>::Success(
|
||||||
|
{std::move(state), std::move(diagnostics)});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,807 @@
|
|||||||
|
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/analysis/newton_iteration_log.h"
|
||||||
|
#include "fesa/analysis/nonlinear_static_control.h"
|
||||||
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/constraints/essential_constraint_policy.h"
|
||||||
|
#include "fesa/elements/finite_rotation.h"
|
||||||
|
#include "fesa/elements/mitc4_shell.h"
|
||||||
|
#include "fesa/elements/nonlinear_mitc4_shell.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/math/sparse_matrix.h"
|
||||||
|
#include "fesa/properties/shell_section.h"
|
||||||
|
#include "fesa/solvers/linear/tangent_solver.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::size_t kDofsPerNode = 6U;
|
||||||
|
constexpr std::size_t kTranslationsPerNode = 3U;
|
||||||
|
constexpr std::size_t kShellDofCount = 24U;
|
||||||
|
constexpr std::uint32_t kMaximumNewtonIterations = 100U;
|
||||||
|
|
||||||
|
struct RuntimeShell {
|
||||||
|
EntityIndex element_index;
|
||||||
|
std::array<std::size_t, kShellDofCount> scatter;
|
||||||
|
NonlinearMitc4Shell element;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InternalAssembly {
|
||||||
|
Vector force;
|
||||||
|
SparseMatrix tangent;
|
||||||
|
std::vector<ShellResultRow> shell_rows;
|
||||||
|
double physical_strain_energy;
|
||||||
|
};
|
||||||
|
|
||||||
|
Status ProcedureFailure(const FailureCategory category, const std::string& code,
|
||||||
|
const SourceLocation& location,
|
||||||
|
const std::string& keyword,
|
||||||
|
const std::string& identity,
|
||||||
|
const std::string& message) {
|
||||||
|
return Status::Failure(category, {{Severity::kError, code, location, keyword,
|
||||||
|
identity, message}});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DiagnosticCode(const Status& status) {
|
||||||
|
if (status.Diagnostics().empty()) {
|
||||||
|
return "nonlinear-procedure-failure";
|
||||||
|
}
|
||||||
|
return status.Diagnostics().front().code;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DiagnosticReason(const Status& status) {
|
||||||
|
if (status.Diagnostics().empty()) {
|
||||||
|
return "The nonlinear procedure returned no diagnostic.";
|
||||||
|
}
|
||||||
|
return status.Diagnostics().front().message;
|
||||||
|
}
|
||||||
|
|
||||||
|
NewtonChannelDecision UnevaluatedDecision(const std::string& identity,
|
||||||
|
const NewtonChannelUnit unit) {
|
||||||
|
return {identity,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
NewtonConvergenceBranch::kZeroScale,
|
||||||
|
kNewtonZeroScaleThreshold,
|
||||||
|
unit,
|
||||||
|
false};
|
||||||
|
}
|
||||||
|
|
||||||
|
NewtonIterationLogRecord NewLogRecord(const StepDefinition& step,
|
||||||
|
const LoadFactorIncrement& increment,
|
||||||
|
const std::uint32_t iteration) {
|
||||||
|
return {step.Name(),
|
||||||
|
increment.increment_index,
|
||||||
|
increment.load_factor,
|
||||||
|
iteration,
|
||||||
|
NewtonIterationStage::kEntered,
|
||||||
|
{false, NewtonIterationStage::kEntered, {}, {}},
|
||||||
|
"entered",
|
||||||
|
"pending",
|
||||||
|
{},
|
||||||
|
UnevaluatedDecision("residual_force", NewtonChannelUnit::kForce),
|
||||||
|
UnevaluatedDecision("residual_moment",
|
||||||
|
NewtonChannelUnit::kForceLength),
|
||||||
|
UnevaluatedDecision("correction_translation",
|
||||||
|
NewtonChannelUnit::kLength),
|
||||||
|
UnevaluatedDecision("correction_rotation",
|
||||||
|
NewtonChannelUnit::kDimensionless),
|
||||||
|
false};
|
||||||
|
}
|
||||||
|
|
||||||
|
Status WriteAndFlush(NewtonIterationLogWriter& log,
|
||||||
|
const NewtonIterationLogRecord& record) {
|
||||||
|
const Status write_status = log.Write(record);
|
||||||
|
if (!write_status.IsOk()) {
|
||||||
|
return write_status;
|
||||||
|
}
|
||||||
|
return log.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
Status RecordFailure(NewtonIterationLogWriter& log,
|
||||||
|
NewtonIterationLogRecord* record, const Status& failure,
|
||||||
|
const NewtonIterationStage stage) {
|
||||||
|
record->last_completed_stage = stage;
|
||||||
|
record->failure = {true, stage, DiagnosticCode(failure),
|
||||||
|
DiagnosticReason(failure)};
|
||||||
|
record->transition = "failed";
|
||||||
|
record->outcome = "failed";
|
||||||
|
record->diagnostic_code = DiagnosticCode(failure);
|
||||||
|
const Status log_status = WriteAndFlush(log, *record);
|
||||||
|
return log_status.IsOk() ? failure : log_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix InitialFrame(const ShellNodeInitialFrame& frame) {
|
||||||
|
Matrix result{3U, 3U};
|
||||||
|
for (std::size_t row = 0U; row < kTranslationsPerNode; ++row) {
|
||||||
|
result(row, 0U) = frame.tangent_a[row];
|
||||||
|
result(row, 1U) = frame.tangent_b[row];
|
||||||
|
result(row, 2U) = frame.director[row];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<DofManager> BuildDofManager(const Domain& domain) {
|
||||||
|
Result<AnalysisModel> model = AnalysisModel::Create(domain);
|
||||||
|
if (!model.HasValue()) {
|
||||||
|
return Result<DofManager>::Failure(model.GetStatus());
|
||||||
|
}
|
||||||
|
return DofManager::Create(model.Value());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<NonlinearStateBaselineInput> BuildBaseline(const Domain& domain) {
|
||||||
|
if (domain.ShellNodeInitialFrames().size() != domain.Nodes().size()) {
|
||||||
|
return Result<NonlinearStateBaselineInput>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "invalid-nonlinear-state",
|
||||||
|
{domain.SourcePath(), 0U},
|
||||||
|
"NODE", domain.SourceContentIdentity(),
|
||||||
|
"Nonlinear analysis requires one initial shell frame per node."));
|
||||||
|
}
|
||||||
|
|
||||||
|
NonlinearStateBaselineInput baseline;
|
||||||
|
baseline.node_baselines.reserve(domain.Nodes().size());
|
||||||
|
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||||
|
const ShellNodeInitialFrame& frame = domain.ShellNodeInitialFrames()[node];
|
||||||
|
if (frame.node_index != static_cast<EntityIndex>(node)) {
|
||||||
|
return Result<NonlinearStateBaselineInput>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "invalid-nonlinear-state",
|
||||||
|
domain.Nodes()[node].location, "NODE",
|
||||||
|
domain.Nodes()[node].source_id.source_label_text,
|
||||||
|
"Initial shell frames must follow stable Domain node order."));
|
||||||
|
}
|
||||||
|
baseline.node_baselines.push_back(
|
||||||
|
{static_cast<EntityIndex>(node), domain.Nodes()[node].source_id,
|
||||||
|
{frame.tangent_a, frame.tangent_b, frame.director}, frame.director});
|
||||||
|
}
|
||||||
|
return Result<NonlinearStateBaselineInput>::Success(std::move(baseline));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<RuntimeShell>> BuildRuntimeShells(const Domain& domain,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
std::vector<RuntimeShell> shells;
|
||||||
|
shells.reserve(domain.Elements().Size());
|
||||||
|
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
|
||||||
|
const auto* definition =
|
||||||
|
dynamic_cast<const Mitc4ShellDefinition*>(&domain.Elements()[index]);
|
||||||
|
if (definition == nullptr) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "unsupported-nonlinear-element",
|
||||||
|
{}, "ELEMENT",
|
||||||
|
domain.Elements()[index].SourceId().source_label_text,
|
||||||
|
"The nonlinear static procedure supports MITC4 shell elements "
|
||||||
|
"only."));
|
||||||
|
}
|
||||||
|
if (definition->section_index >= domain.Properties().Size() ||
|
||||||
|
definition->material_index >= domain.Materials().Size()) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "invalid-nonlinear-static-data",
|
||||||
|
definition->location, "ELEMENT",
|
||||||
|
definition->source_id.source_label_text,
|
||||||
|
"The nonlinear shell element property or material index is "
|
||||||
|
"invalid."));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* section =
|
||||||
|
dynamic_cast<const ShellSection*>(
|
||||||
|
&domain.Properties()[definition->section_index]);
|
||||||
|
const auto* material = dynamic_cast<const LinearElasticMaterial*>(
|
||||||
|
&domain.Materials()[definition->material_index]);
|
||||||
|
if (section == nullptr || material == nullptr) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "incompatible-nonlinear-shell-property",
|
||||||
|
definition->location, "ELEMENT",
|
||||||
|
definition->source_id.source_label_text,
|
||||||
|
"A nonlinear MITC4 shell requires a shell section and linear "
|
||||||
|
"elastic material."));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<const Node*, 4U> nodes{};
|
||||||
|
std::array<ShellNodeInitialFrame, 4U> frames{};
|
||||||
|
for (std::size_t node = 0U; node < definition->node_indices.size();
|
||||||
|
++node) {
|
||||||
|
const EntityIndex node_index = definition->node_indices[node];
|
||||||
|
if (node_index >= domain.Nodes().size() ||
|
||||||
|
node_index >= domain.ShellNodeInitialFrames().size()) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "invalid-shell-director",
|
||||||
|
definition->location, "ELEMENT",
|
||||||
|
definition->source_id.source_label_text,
|
||||||
|
"A nonlinear shell node does not have a stable initial frame."));
|
||||||
|
}
|
||||||
|
nodes[node] = &domain.Nodes()[node_index];
|
||||||
|
frames[node] = domain.ShellNodeInitialFrames()[node_index];
|
||||||
|
if (frames[node].node_index != node_index) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kModel, "invalid-shell-director",
|
||||||
|
definition->location, "ELEMENT",
|
||||||
|
definition->source_id.source_label_text,
|
||||||
|
"A nonlinear shell initial frame does not match its node "
|
||||||
|
"identity."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<NonlinearMitc4Shell> shell =
|
||||||
|
NonlinearMitc4Shell::Create(nodes, frames, *section, *material);
|
||||||
|
if (!shell.HasValue()) {
|
||||||
|
return Result<std::vector<RuntimeShell>>::Failure(shell.GetStatus());
|
||||||
|
}
|
||||||
|
const EntityIndex element_index = static_cast<EntityIndex>(index);
|
||||||
|
shells.push_back(
|
||||||
|
{element_index, dofs.ShellElementScatter(element_index),
|
||||||
|
std::move(shell.Value())});
|
||||||
|
}
|
||||||
|
return Result<std::vector<RuntimeShell>>::Success(std::move(shells));
|
||||||
|
}
|
||||||
|
|
||||||
|
Status PopulateNodalKinematics(const Domain& domain,
|
||||||
|
NonlinearTrialState* trial) {
|
||||||
|
if (trial->generalized_coordinates.Size() !=
|
||||||
|
domain.Nodes().size() * kDofsPerNode) {
|
||||||
|
return ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data",
|
||||||
|
{domain.SourcePath(), 0U},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", domain.SourceContentIdentity(),
|
||||||
|
"Nonlinear trial coordinates do not match the Domain node inventory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
trial->displacement = trial->generalized_coordinates;
|
||||||
|
trial->rotation_vector = Vector{trial->generalized_coordinates.Size()};
|
||||||
|
trial->nodal_triad.clear();
|
||||||
|
trial->nodal_director.clear();
|
||||||
|
trial->nodal_triad.reserve(domain.Nodes().size());
|
||||||
|
trial->nodal_director.reserve(domain.Nodes().size());
|
||||||
|
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||||
|
const std::size_t first = node * kDofsPerNode;
|
||||||
|
const Vector3 phi{trial->generalized_coordinates[first + 3U],
|
||||||
|
trial->generalized_coordinates[first + 4U],
|
||||||
|
trial->generalized_coordinates[first + 5U]};
|
||||||
|
Result<FiniteRotationState> rotation = MakeFiniteRotationState(
|
||||||
|
phi, InitialFrame(domain.ShellNodeInitialFrames()[node]));
|
||||||
|
if (!rotation.HasValue()) {
|
||||||
|
return rotation.GetStatus();
|
||||||
|
}
|
||||||
|
for (std::size_t component = 0U; component < kTranslationsPerNode;
|
||||||
|
++component) {
|
||||||
|
trial->rotation_vector[first + 3U + component] = phi[component];
|
||||||
|
}
|
||||||
|
const Matrix& q = rotation.Value().q;
|
||||||
|
trial->nodal_triad.push_back(
|
||||||
|
{{{q(0U, 0U), q(1U, 0U), q(2U, 0U)},
|
||||||
|
{q(0U, 1U), q(1U, 1U), q(2U, 1U)},
|
||||||
|
{q(0U, 2U), q(1U, 2U), q(2U, 2U)}}});
|
||||||
|
trial->nodal_director.push_back(rotation.Value().director.Components());
|
||||||
|
}
|
||||||
|
return Status::Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
NonlinearMitc4TrialDofs ExtractElementTrial(
|
||||||
|
const RuntimeShell& shell, const Vector& coordinates) {
|
||||||
|
NonlinearMitc4TrialDofs trial{};
|
||||||
|
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||||
|
const std::size_t first = node * kDofsPerNode;
|
||||||
|
trial.displacement[node] =
|
||||||
|
{coordinates[shell.scatter[first]],
|
||||||
|
coordinates[shell.scatter[first + 1U]],
|
||||||
|
coordinates[shell.scatter[first + 2U]]};
|
||||||
|
trial.rotation_vector[node] =
|
||||||
|
{coordinates[shell.scatter[first + 3U]],
|
||||||
|
coordinates[shell.scatter[first + 4U]],
|
||||||
|
coordinates[shell.scatter[first + 5U]]};
|
||||||
|
}
|
||||||
|
return trial;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFinite(const Vector& values) {
|
||||||
|
for (std::size_t index = 0U; index < values.Size(); ++index) {
|
||||||
|
if (!std::isfinite(values[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFinite(const Matrix& values) {
|
||||||
|
for (std::size_t row = 0U; row < values.Rows(); ++row) {
|
||||||
|
for (std::size_t column = 0U; column < values.Columns(); ++column) {
|
||||||
|
if (!std::isfinite(values(row, column))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<InternalAssembly> AssembleInternal(
|
||||||
|
const std::vector<RuntimeShell>& shells, const DofManager& dofs,
|
||||||
|
const Vector& coordinates, const ParallelFor& parallel_for) {
|
||||||
|
std::vector<std::optional<NonlinearMitc4InternalEquilibrium>> local(
|
||||||
|
shells.size());
|
||||||
|
std::vector<std::optional<Status>> failures(shells.size());
|
||||||
|
parallel_for.Execute(shells.size(), [&](const std::size_t index) {
|
||||||
|
Result<NonlinearMitc4InternalEquilibrium> result =
|
||||||
|
EvaluateInternalResidualAndTangent(shells[index].element,
|
||||||
|
ExtractElementTrial(shells[index],
|
||||||
|
coordinates));
|
||||||
|
if (!result.HasValue()) {
|
||||||
|
failures[index] = result.GetStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
local[index] = std::move(result.Value());
|
||||||
|
});
|
||||||
|
|
||||||
|
Vector force{dofs.FullDofCount()};
|
||||||
|
std::vector<CooContribution> contributions;
|
||||||
|
contributions.reserve(shells.size() * kShellDofCount * kShellDofCount);
|
||||||
|
std::vector<ShellResultRow> rows;
|
||||||
|
rows.reserve(shells.size() * 4U);
|
||||||
|
double energy = 0.0;
|
||||||
|
for (std::size_t index = 0U; index < shells.size(); ++index) {
|
||||||
|
if (failures[index].has_value()) {
|
||||||
|
return Result<InternalAssembly>::Failure(*failures[index]);
|
||||||
|
}
|
||||||
|
if (!local[index].has_value() ||
|
||||||
|
!IsFinite(local[index]->internal_residual) ||
|
||||||
|
!IsFinite(local[index]->internal_tangent) ||
|
||||||
|
!std::isfinite(local[index]->physical_strain_energy)) {
|
||||||
|
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", std::to_string(index),
|
||||||
|
"A nonlinear shell internal residual or tangent is nonfinite."));
|
||||||
|
}
|
||||||
|
for (std::size_t row = 0U; row < kShellDofCount; ++row) {
|
||||||
|
force[shells[index].scatter[row]] += local[index]->internal_residual[row];
|
||||||
|
for (std::size_t column = 0U; column < kShellDofCount; ++column) {
|
||||||
|
contributions.push_back(
|
||||||
|
{shells[index].scatter[row], shells[index].scatter[column],
|
||||||
|
local[index]->internal_tangent(row, column), index,
|
||||||
|
row * kShellDofCount + column});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const NonlinearMitc4RecoveryPoint& point :
|
||||||
|
local[index]->recovery.gauss_points) {
|
||||||
|
rows.push_back({shells[index].element_index,
|
||||||
|
point.location,
|
||||||
|
point.natural_coordinates,
|
||||||
|
{point.local_frame.e1, point.local_frame.e2,
|
||||||
|
point.local_frame.e3},
|
||||||
|
point.generalized_strain,
|
||||||
|
point.section_resultant,
|
||||||
|
point.stress});
|
||||||
|
}
|
||||||
|
energy += local[index]->physical_strain_energy;
|
||||||
|
}
|
||||||
|
if (!IsFinite(force) || !std::isfinite(energy)) {
|
||||||
|
return Result<InternalAssembly>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||||
|
"Global nonlinear internal force or energy is nonfinite."));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SparseMatrix> tangent = SparseMatrix::FromCoo(
|
||||||
|
dofs.FullDofCount(), dofs.FullDofCount(), std::move(contributions),
|
||||||
|
dofs.GetSparsePattern());
|
||||||
|
if (!tangent.HasValue()) {
|
||||||
|
return Result<InternalAssembly>::Failure(tangent.GetStatus());
|
||||||
|
}
|
||||||
|
return Result<InternalAssembly>::Success(
|
||||||
|
{std::move(force), std::move(tangent.Value()), std::move(rows), energy});
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Vector> Difference(const Vector& left, const Vector& right) {
|
||||||
|
if (left.Size() != right.Size()) {
|
||||||
|
return Result<Vector>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||||
|
"Nonlinear internal and external vectors must have equal dimensions."));
|
||||||
|
}
|
||||||
|
Vector result{left.Size()};
|
||||||
|
for (std::size_t index = 0U; index < result.Size(); ++index) {
|
||||||
|
result[index] = left[index] - right[index];
|
||||||
|
}
|
||||||
|
if (!IsFinite(result)) {
|
||||||
|
return Result<Vector>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||||
|
"Nonlinear residual must remain finite."));
|
||||||
|
}
|
||||||
|
return Result<Vector>::Success(std::move(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<SparseMatrix> Difference(const SparseMatrix& internal,
|
||||||
|
const SparseMatrix& external,
|
||||||
|
const DofManager& dofs) {
|
||||||
|
if (internal.Rows() != external.Rows() ||
|
||||||
|
internal.Columns() != external.Columns() ||
|
||||||
|
internal.RowOffsets() != external.RowOffsets() ||
|
||||||
|
internal.ColumnIndices() != external.ColumnIndices()) {
|
||||||
|
return Result<SparseMatrix>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||||
|
"Nonlinear internal and external tangent patterns must match."));
|
||||||
|
}
|
||||||
|
std::vector<CooContribution> contributions;
|
||||||
|
contributions.reserve(internal.Values().size());
|
||||||
|
for (std::size_t row = 0U; row < internal.Rows(); ++row) {
|
||||||
|
for (std::size_t offset = internal.RowOffsets()[row];
|
||||||
|
offset < internal.RowOffsets()[row + 1U]; ++offset) {
|
||||||
|
const double value =
|
||||||
|
internal.Values()[offset] - external.Values()[offset];
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
return Result<SparseMatrix>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", {},
|
||||||
|
"Nonlinear tangent must remain finite."));
|
||||||
|
}
|
||||||
|
contributions.push_back(
|
||||||
|
{row, internal.ColumnIndices()[offset], value, row, offset});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SparseMatrix::FromCoo(internal.Rows(), internal.Columns(),
|
||||||
|
std::move(contributions),
|
||||||
|
dofs.GetSparsePattern());
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector SelectFreeComponents(const Vector& full, const DofManager& dofs,
|
||||||
|
const bool rotations) {
|
||||||
|
std::size_t count = 0U;
|
||||||
|
for (const std::size_t full_dof : dofs.FreeDofs()) {
|
||||||
|
if ((full_dof % kDofsPerNode >= kTranslationsPerNode) == rotations) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vector selected{count};
|
||||||
|
std::size_t output = 0U;
|
||||||
|
for (const std::size_t full_dof : dofs.FreeDofs()) {
|
||||||
|
if ((full_dof % kDofsPerNode >= kTranslationsPerNode) == rotations) {
|
||||||
|
selected[output++] = full[full_dof];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector SelectCorrectionComponents(const Vector& correction,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const bool rotations) {
|
||||||
|
std::size_t count = 0U;
|
||||||
|
for (const std::size_t full_dof : dofs.FreeDofs()) {
|
||||||
|
if ((full_dof % kDofsPerNode >= kTranslationsPerNode) == rotations) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Vector selected{count};
|
||||||
|
std::size_t output = 0U;
|
||||||
|
for (std::size_t equation = 0U; equation < dofs.FreeDofs().size();
|
||||||
|
++equation) {
|
||||||
|
if ((dofs.FreeDofs()[equation] % kDofsPerNode >=
|
||||||
|
kTranslationsPerNode) == rotations) {
|
||||||
|
selected[output++] = correction[equation];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector CurrentIncrementComponents(const Vector& current, const Vector& start,
|
||||||
|
const DofManager& dofs,
|
||||||
|
const bool rotations) {
|
||||||
|
Vector difference{current.Size()};
|
||||||
|
for (std::size_t index = 0U; index < difference.Size(); ++index) {
|
||||||
|
difference[index] = current[index] - start[index];
|
||||||
|
}
|
||||||
|
return SelectFreeComponents(difference, dofs, rotations);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<double, 6> GlobalEquilibrium(const Domain& domain,
|
||||||
|
const Vector& residual) {
|
||||||
|
std::array<double, 6> equilibrium{};
|
||||||
|
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
||||||
|
const std::size_t first = node * kDofsPerNode;
|
||||||
|
const Vector3 force{residual[first], residual[first + 1U],
|
||||||
|
residual[first + 2U]};
|
||||||
|
const Vector3 position{domain.Nodes()[node].coordinates};
|
||||||
|
const Vector3 moment = position.Cross(force);
|
||||||
|
equilibrium[0U] += force.X();
|
||||||
|
equilibrium[1U] += force.Y();
|
||||||
|
equilibrium[2U] += force.Z();
|
||||||
|
equilibrium[3U] += moment.X() + residual[first + 3U];
|
||||||
|
equilibrium[4U] += moment.Y() + residual[first + 4U];
|
||||||
|
equilibrium[5U] += moment.Z() + residual[first + 5U];
|
||||||
|
}
|
||||||
|
return equilibrium;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status ApplyIncrementConstraints(const StepDefinition& step,
|
||||||
|
const LoadFactorIncrement& increment,
|
||||||
|
const DofManager& dofs,
|
||||||
|
NonlinearTrialState* trial) {
|
||||||
|
Result<Vector> source_targets =
|
||||||
|
ApplyAbsolutePrescribedDisplacementTarget(step, increment.load_factor);
|
||||||
|
if (!source_targets.HasValue()) {
|
||||||
|
return source_targets.GetStatus();
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size();
|
||||||
|
++index) {
|
||||||
|
const double target =
|
||||||
|
increment.load_factor * dofs.PrescribedValues()[index];
|
||||||
|
if (!std::isfinite(target)) {
|
||||||
|
return ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", step.Location(),
|
||||||
|
"BOUNDARY", step.Name(),
|
||||||
|
"A nonlinear prescribed displacement target is nonfinite.");
|
||||||
|
}
|
||||||
|
trial->generalized_coordinates[dofs.ConstrainedDofs()[index]] = target;
|
||||||
|
}
|
||||||
|
return Status::Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
AcceptedFrameState AcceptedFrame(const NonlinearAnalysisState& state,
|
||||||
|
const NonlinearTrialState& trial,
|
||||||
|
const LoadFactorIncrement& increment,
|
||||||
|
const std::uint32_t iteration) {
|
||||||
|
return {static_cast<std::uint32_t>(state.AcceptedFrames().size()),
|
||||||
|
increment.increment_index,
|
||||||
|
increment.load_factor,
|
||||||
|
iteration,
|
||||||
|
increment.is_factor_one,
|
||||||
|
trial.nodal_internal_order,
|
||||||
|
trial.nodal_source_ids,
|
||||||
|
trial.generalized_coordinates,
|
||||||
|
trial.displacement,
|
||||||
|
trial.rotation_vector,
|
||||||
|
trial.nodal_triad,
|
||||||
|
trial.nodal_director,
|
||||||
|
trial.internal_force,
|
||||||
|
trial.external_force,
|
||||||
|
trial.residual,
|
||||||
|
trial.reaction,
|
||||||
|
trial.shell_results,
|
||||||
|
trial.physical_strain_energy,
|
||||||
|
trial.global_equilibrium};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
NonlinearStaticProcedureExecutor::NonlinearStaticProcedureExecutor(
|
||||||
|
const ParallelFor& parallel_for, TangentSolver& tangent_solver)
|
||||||
|
: parallel_for_(parallel_for), tangent_solver_(tangent_solver) {}
|
||||||
|
|
||||||
|
Result<NonlinearStaticExecutionResult>
|
||||||
|
NonlinearStaticProcedureExecutor::Execute(
|
||||||
|
const Domain& domain, NewtonIterationLogWriter& log) const {
|
||||||
|
if (domain.Steps().Size() != 1U || !domain.Steps()[0U].IsNonlinearStatic()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(ProcedureFailure(
|
||||||
|
FailureCategory::kInput, "invalid-nonlinear-static-data",
|
||||||
|
{domain.SourcePath(), 0U}, "STATIC", domain.SourceContentIdentity(),
|
||||||
|
"Nonlinear procedure execution requires exactly one NLGEOM static "
|
||||||
|
"step."));
|
||||||
|
}
|
||||||
|
const StepDefinition& step = domain.Steps()[0U];
|
||||||
|
|
||||||
|
Result<DofManager> dofs = BuildDofManager(domain);
|
||||||
|
if (!dofs.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(dofs.GetStatus());
|
||||||
|
}
|
||||||
|
Result<NonlinearStateBaselineInput> baseline = BuildBaseline(domain);
|
||||||
|
if (!baseline.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(
|
||||||
|
baseline.GetStatus());
|
||||||
|
}
|
||||||
|
Result<NonlinearAnalysisState> state =
|
||||||
|
NonlinearAnalysisState::Create(domain, dofs.Value(), baseline.Value());
|
||||||
|
if (!state.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(state.GetStatus());
|
||||||
|
}
|
||||||
|
Result<std::vector<RuntimeShell>> shells =
|
||||||
|
BuildRuntimeShells(domain, dofs.Value());
|
||||||
|
if (!shells.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(shells.GetStatus());
|
||||||
|
}
|
||||||
|
Result<std::vector<LoadFactorIncrement>> increments =
|
||||||
|
BuildFixedLoadFactorSequence(step);
|
||||||
|
if (!increments.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(
|
||||||
|
increments.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
const EssentialConstraintPolicy constraints;
|
||||||
|
for (const LoadFactorIncrement& increment : increments.Value()) {
|
||||||
|
Result<NonlinearTrialState> trial = state.Value().BeginTrialFromCommitted();
|
||||||
|
if (!trial.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(trial.GetStatus());
|
||||||
|
}
|
||||||
|
const Vector increment_start = trial.Value().generalized_coordinates;
|
||||||
|
Status constraint_status =
|
||||||
|
ApplyIncrementConstraints(step, increment, dofs.Value(),
|
||||||
|
&trial.Value());
|
||||||
|
if (!constraint_status.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(constraint_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::uint32_t iteration = 1U; iteration <= kMaximumNewtonIterations;
|
||||||
|
++iteration) {
|
||||||
|
NewtonIterationLogRecord record =
|
||||||
|
NewLogRecord(step, increment, iteration);
|
||||||
|
trial.Value().increment_index = increment.increment_index;
|
||||||
|
trial.Value().load_factor = increment.load_factor;
|
||||||
|
trial.Value().is_factor_zero_baseline = false;
|
||||||
|
|
||||||
|
Status kinematics = PopulateNodalKinematics(domain, &trial.Value());
|
||||||
|
if (!kinematics.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(
|
||||||
|
RecordFailure(log, &record, kinematics,
|
||||||
|
NewtonIterationStage::kKinematicsEvaluated));
|
||||||
|
}
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kKinematicsEvaluated;
|
||||||
|
|
||||||
|
Result<InternalAssembly> internal = AssembleInternal(
|
||||||
|
shells.Value(), dofs.Value(), trial.Value().generalized_coordinates,
|
||||||
|
parallel_for_);
|
||||||
|
if (!internal.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, internal.GetStatus(),
|
||||||
|
NewtonIterationStage::kInternalAssembled));
|
||||||
|
}
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kInternalAssembled;
|
||||||
|
|
||||||
|
Result<GlobalFixedLoadContribution> external =
|
||||||
|
AssembleGlobalFixedLoadContribution(
|
||||||
|
domain, dofs.Value(), trial.Value().generalized_coordinates,
|
||||||
|
increment.load_factor);
|
||||||
|
if (!external.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, external.GetStatus(),
|
||||||
|
NewtonIterationStage::kExternalAssembled));
|
||||||
|
}
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kExternalAssembled;
|
||||||
|
|
||||||
|
Result<Vector> residual =
|
||||||
|
Difference(internal.Value().force,
|
||||||
|
external.Value().generalized_external_force);
|
||||||
|
if (!residual.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, residual.GetStatus(),
|
||||||
|
NewtonIterationStage::kExternalAssembled));
|
||||||
|
}
|
||||||
|
Result<SparseMatrix> tangent =
|
||||||
|
Difference(internal.Value().tangent,
|
||||||
|
external.Value().generalized_external_tangent,
|
||||||
|
dofs.Value());
|
||||||
|
if (!tangent.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, tangent.GetStatus(),
|
||||||
|
NewtonIterationStage::kExternalAssembled));
|
||||||
|
}
|
||||||
|
trial.Value().internal_force = std::move(internal.Value().force);
|
||||||
|
trial.Value().external_force =
|
||||||
|
std::move(external.Value().generalized_external_force);
|
||||||
|
trial.Value().residual = std::move(residual.Value());
|
||||||
|
trial.Value().reaction = trial.Value().residual;
|
||||||
|
trial.Value().shell_results = std::move(internal.Value().shell_rows);
|
||||||
|
trial.Value().physical_strain_energy =
|
||||||
|
internal.Value().physical_strain_energy;
|
||||||
|
trial.Value().global_equilibrium =
|
||||||
|
GlobalEquilibrium(domain, trial.Value().residual);
|
||||||
|
|
||||||
|
Result<PartitionedStiffness> partition =
|
||||||
|
constraints.Partition(tangent.Value(), dofs.Value());
|
||||||
|
if (!partition.HasValue()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, partition.GetStatus(),
|
||||||
|
NewtonIterationStage::kExternalAssembled));
|
||||||
|
}
|
||||||
|
const Status factorization = tangent_solver_.FactorizeFresh(
|
||||||
|
partition.Value().k_ff);
|
||||||
|
if (!factorization.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, factorization,
|
||||||
|
NewtonIterationStage::kSolvedCorrection));
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector correction_rhs = constraints.GatherFree(trial.Value().residual,
|
||||||
|
dofs.Value());
|
||||||
|
for (std::size_t index = 0U; index < correction_rhs.Size(); ++index) {
|
||||||
|
correction_rhs[index] = -correction_rhs[index];
|
||||||
|
}
|
||||||
|
Vector correction{dofs.Value().FreeDofCount()};
|
||||||
|
const Status solve =
|
||||||
|
tangent_solver_.SolveCorrection(correction_rhs, correction);
|
||||||
|
if (!solve.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, solve, NewtonIterationStage::kSolvedCorrection));
|
||||||
|
}
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kSolvedCorrection;
|
||||||
|
|
||||||
|
const NewtonConvergenceResult convergence = EvaluateNewtonConvergence({
|
||||||
|
{"residual_force", SelectFreeComponents(trial.Value().residual,
|
||||||
|
dofs.Value(), false),
|
||||||
|
SelectFreeComponents(trial.Value().internal_force, dofs.Value(),
|
||||||
|
false),
|
||||||
|
SelectFreeComponents(trial.Value().external_force, dofs.Value(),
|
||||||
|
false)},
|
||||||
|
{"residual_moment", SelectFreeComponents(trial.Value().residual,
|
||||||
|
dofs.Value(), true),
|
||||||
|
SelectFreeComponents(trial.Value().internal_force, dofs.Value(),
|
||||||
|
true),
|
||||||
|
SelectFreeComponents(trial.Value().external_force, dofs.Value(),
|
||||||
|
true)},
|
||||||
|
{"correction_translation",
|
||||||
|
SelectCorrectionComponents(correction, dofs.Value(), false),
|
||||||
|
CurrentIncrementComponents(trial.Value().generalized_coordinates,
|
||||||
|
increment_start, dofs.Value(), false)},
|
||||||
|
{"correction_rotation",
|
||||||
|
SelectCorrectionComponents(correction, dofs.Value(), true),
|
||||||
|
CurrentIncrementComponents(trial.Value().generalized_coordinates,
|
||||||
|
increment_start, dofs.Value(), true)}});
|
||||||
|
record.force_residual = convergence.force_residual;
|
||||||
|
record.moment_residual = convergence.moment_residual;
|
||||||
|
record.translation_correction = convergence.translation_correction;
|
||||||
|
record.rotation_correction = convergence.rotation_correction;
|
||||||
|
record.combined_passed = convergence.combined_passed;
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kConvergenceEvaluated;
|
||||||
|
|
||||||
|
if (convergence.combined_passed) {
|
||||||
|
const Status committed = state.Value().CommitAcceptedFrame(
|
||||||
|
AcceptedFrame(state.Value(), trial.Value(), increment, iteration));
|
||||||
|
if (!committed.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, committed, NewtonIterationStage::kAccepted));
|
||||||
|
}
|
||||||
|
record.last_completed_stage = NewtonIterationStage::kAccepted;
|
||||||
|
record.transition = "accepted";
|
||||||
|
record.outcome = "accepted";
|
||||||
|
const Status log_status = WriteAndFlush(log, record);
|
||||||
|
if (!log_status.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(log_status);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iteration == kMaximumNewtonIterations) {
|
||||||
|
const Status failure = ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonlinear-nonconvergence",
|
||||||
|
step.Location(), "STATIC", step.Name(),
|
||||||
|
"The nonlinear procedure did not converge within 100 iterations.");
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, failure,
|
||||||
|
NewtonIterationStage::kConvergenceEvaluated));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t equation = 0U; equation < dofs.Value().FreeDofs().size();
|
||||||
|
++equation) {
|
||||||
|
const std::size_t full_dof = dofs.Value().FreeDofs()[equation];
|
||||||
|
trial.Value().generalized_coordinates[full_dof] += correction[equation];
|
||||||
|
if (!std::isfinite(trial.Value().generalized_coordinates[full_dof])) {
|
||||||
|
const Status failure = ProcedureFailure(
|
||||||
|
FailureCategory::kSolver, "nonfinite-newton-data", {},
|
||||||
|
"NONLINEAR_STATIC_PROCEDURE", std::to_string(full_dof),
|
||||||
|
"A nonlinear free-coordinate correction is nonfinite.");
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(RecordFailure(
|
||||||
|
log, &record, failure, NewtonIterationStage::kSolvedCorrection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record.transition = "continue";
|
||||||
|
record.outcome = "not-converged";
|
||||||
|
const Status log_status = WriteAndFlush(log, record);
|
||||||
|
if (!log_status.IsOk()) {
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Failure(log_status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Diagnostic> diagnostics = domain.Warnings();
|
||||||
|
SortDiagnostics(diagnostics);
|
||||||
|
return Result<NonlinearStaticExecutionResult>::Success(
|
||||||
|
{std::move(state.Value()), std::move(diagnostics)});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
|
#include "fesa/analysis/newton_iteration_log.h"
|
||||||
|
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||||
|
#include "fesa/app/output_paths.h"
|
||||||
|
#include "fesa/io/abaqus/domain_mapper.h"
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
#include "fesa/results/nonlinear_results_writer.h"
|
||||||
|
#include "fesa/results/results_writer.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
Result<PreparedAnalysisInput> PrepareAnalysisInput(
|
||||||
|
const std::filesystem::path& input_path) {
|
||||||
|
const Result<ParsedInput> syntax = AbaqusInputReader{}.Read(input_path);
|
||||||
|
if (!syntax.HasValue()) {
|
||||||
|
return Result<PreparedAnalysisInput>::Failure(syntax.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<Domain> mapped = AbaqusDomainMapper{}.Map(syntax.Value());
|
||||||
|
if (!mapped.HasValue()) {
|
||||||
|
return Result<PreparedAnalysisInput>::Failure(mapped.GetStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
PreparedAnalysisInput prepared{std::move(mapped.Value()), {}};
|
||||||
|
prepared.diagnostics = prepared.domain.Warnings();
|
||||||
|
SortDiagnostics(prepared.diagnostics);
|
||||||
|
return Result<PreparedAnalysisInput>::Success(std::move(prepared));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<PreparedAnalysisOutcome> ExecutePreparedAnalysis(
|
||||||
|
const Domain& domain, LinearStaticProcedureExecutor& linear_executor,
|
||||||
|
NonlinearStaticProcedureExecutor& nonlinear_executor,
|
||||||
|
NewtonIterationLogWriter& log) {
|
||||||
|
if (domain.Steps().Size() != 1U) {
|
||||||
|
return Result<PreparedAnalysisOutcome>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kInput,
|
||||||
|
{{Severity::kError, "invalid-analysis-step-count", {}, "*STEP", {},
|
||||||
|
"Prepared analysis requires exactly one static step."}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.Steps()[0U].IsNonlinearStatic()) {
|
||||||
|
Result<NonlinearStaticExecutionResult> nonlinear =
|
||||||
|
nonlinear_executor.Execute(domain, log);
|
||||||
|
if (!nonlinear.HasValue()) {
|
||||||
|
return Result<PreparedAnalysisOutcome>::Failure(nonlinear.GetStatus());
|
||||||
|
}
|
||||||
|
return Result<PreparedAnalysisOutcome>::Success(
|
||||||
|
PreparedAnalysisOutcome{std::move(nonlinear.Value())});
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<LinearStaticExecutionResult> linear = linear_executor.Execute(domain);
|
||||||
|
if (!linear.HasValue()) {
|
||||||
|
return Result<PreparedAnalysisOutcome>::Failure(linear.GetStatus());
|
||||||
|
}
|
||||||
|
return Result<PreparedAnalysisOutcome>::Success(
|
||||||
|
PreparedAnalysisOutcome{std::move(linear.Value())});
|
||||||
|
}
|
||||||
|
|
||||||
|
Status FinalizePreparedAnalysisResult(
|
||||||
|
const DerivedOutputPaths& paths, const Domain& domain,
|
||||||
|
const PreparedAnalysisOutcome& outcome,
|
||||||
|
const std::vector<Diagnostic>& diagnostics, ResultsWriter& linear_writer,
|
||||||
|
NonlinearResultsWriter& nonlinear_writer) {
|
||||||
|
if (std::holds_alternative<LinearStaticExecutionResult>(outcome)) {
|
||||||
|
return linear_writer.Write(
|
||||||
|
paths.results_path, domain,
|
||||||
|
std::get<LinearStaticExecutionResult>(outcome).state, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonlinear_writer.Write(
|
||||||
|
paths.results_path, domain,
|
||||||
|
std::get<NonlinearStaticExecutionResult>(outcome).state, diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -2,17 +2,20 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "fesa/analysis/analysis.h"
|
#include "fesa/analysis/linear_static_procedure.h"
|
||||||
#include "fesa/analysis/linear_static_analysis.h"
|
#include "fesa/analysis/newton_iteration_log.h"
|
||||||
|
#include "fesa/analysis/nonlinear_static_procedure.h"
|
||||||
|
#include "fesa/analysis/prepared_analysis.h"
|
||||||
#include "fesa/app/output_paths.h"
|
#include "fesa/app/output_paths.h"
|
||||||
#include "fesa/assembly/parallel_for.h"
|
#include "fesa/assembly/parallel_for.h"
|
||||||
#include "fesa/core/diagnostic.h"
|
#include "fesa/core/diagnostic.h"
|
||||||
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
||||||
|
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
|
||||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||||
|
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -73,17 +76,53 @@ int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
|||||||
return kOutputExitCode;
|
return kOutputExitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalysisRequest request;
|
|
||||||
request.input_path = paths.Value().input_path;
|
|
||||||
request.output_path = paths.Value().results_path;
|
|
||||||
request.log_path = paths.Value().log_path;
|
|
||||||
|
|
||||||
TbbParallelFor parallel_for;
|
TbbParallelFor parallel_for;
|
||||||
MklPardisoSolver linear_solver;
|
MklPardisoSolver linear_solver;
|
||||||
Hdf5ResultsWriter results_writer;
|
MklPardisoTangentSolver tangent_solver;
|
||||||
std::unique_ptr<Analysis> analysis = std::make_unique<LinearStaticAnalysis>(
|
Hdf5ResultsWriter linear_writer;
|
||||||
parallel_for, linear_solver, results_writer);
|
NonlinearHdf5ResultsWriter nonlinear_writer;
|
||||||
const Status status = analysis->Run(request);
|
NewtonIterationLogWriter log;
|
||||||
|
|
||||||
|
Status status = log.Open(paths.Value().log_path);
|
||||||
|
if (!status.IsOk()) {
|
||||||
|
WriteDiagnostics(status.Diagnostics());
|
||||||
|
return ExitCodeFor(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<PreparedAnalysisInput> prepared =
|
||||||
|
PrepareAnalysisInput(paths.Value().input_path);
|
||||||
|
if (!prepared.HasValue()) {
|
||||||
|
status = prepared.GetStatus();
|
||||||
|
} else {
|
||||||
|
LinearStaticProcedureExecutor linear_executor(parallel_for, linear_solver);
|
||||||
|
NonlinearStaticProcedureExecutor nonlinear_executor(parallel_for,
|
||||||
|
tangent_solver);
|
||||||
|
Result<PreparedAnalysisOutcome> outcome = ExecutePreparedAnalysis(
|
||||||
|
prepared.Value().domain, linear_executor, nonlinear_executor, log);
|
||||||
|
if (!outcome.HasValue()) {
|
||||||
|
status = outcome.GetStatus();
|
||||||
|
} else {
|
||||||
|
const Status close_status = log.Close();
|
||||||
|
if (!close_status.IsOk()) {
|
||||||
|
WriteDiagnostics(close_status.Diagnostics());
|
||||||
|
return ExitCodeFor(close_status);
|
||||||
|
}
|
||||||
|
status = FinalizePreparedAnalysisResult(
|
||||||
|
paths.Value(), prepared.Value().domain, outcome.Value(),
|
||||||
|
prepared.Value().diagnostics, linear_writer, nonlinear_writer);
|
||||||
|
if (status.IsOk()) {
|
||||||
|
return kSuccessExitCode;
|
||||||
|
}
|
||||||
|
WriteDiagnostics(status.Diagnostics());
|
||||||
|
return ExitCodeFor(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Status close_status = log.Close();
|
||||||
|
if (!close_status.IsOk()) {
|
||||||
|
WriteDiagnostics(close_status.Diagnostics());
|
||||||
|
return ExitCodeFor(close_status);
|
||||||
|
}
|
||||||
if (status.IsOk()) {
|
if (status.IsOk()) {
|
||||||
return kSuccessExitCode;
|
return kSuccessExitCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ target_link_libraries(
|
|||||||
add_executable(
|
add_executable(
|
||||||
fesa_integration_tests
|
fesa_integration_tests
|
||||||
integration/analysis/linear_static_analysis_test.cpp
|
integration/analysis/linear_static_analysis_test.cpp
|
||||||
|
integration/analysis/nonlinear_static_procedure_test.cpp
|
||||||
integration/app/fesa_application_test.cpp
|
integration/app/fesa_application_test.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
#include "fesa/analysis/analysis.h"
|
#include "fesa/analysis/analysis.h"
|
||||||
#include "fesa/assembly/parallel_for.h"
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/math/sparse_matrix.h"
|
||||||
#include "fesa/results/results_writer.h"
|
#include "fesa/results/results_writer.h"
|
||||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TempDirectory {
|
||||||
|
public:
|
||||||
|
explicit TempDirectory(const std::string& label) {
|
||||||
|
static std::atomic<unsigned long long> sequence{0U};
|
||||||
|
const auto tick =
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
|
path_ = std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-step10-nonlinear-" + label + "-" +
|
||||||
|
std::to_string(tick) + "-" +
|
||||||
|
std::to_string(sequence.fetch_add(1U)));
|
||||||
|
std::error_code error;
|
||||||
|
if (!std::filesystem::create_directory(path_, error) || error) {
|
||||||
|
throw std::runtime_error{"Unable to create nonlinear procedure fixture."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TempDirectory(const TempDirectory&) = delete;
|
||||||
|
TempDirectory& operator=(const TempDirectory&) = delete;
|
||||||
|
|
||||||
|
~TempDirectory() {
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove_all(path_, ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
void WriteText(const std::filesystem::path& path, const std::string& text) {
|
||||||
|
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
||||||
|
stream.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||||
|
if (!stream) {
|
||||||
|
throw std::runtime_error{"Unable to write nonlinear procedure input."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AllConstrainedNonlinearShellDeck() {
|
||||||
|
return R"inp(*Part, name=ShellPart
|
||||||
|
*Node
|
||||||
|
1, 0., 0., 0.
|
||||||
|
2, 1., 0., 0.
|
||||||
|
3, 1., 1., 0.
|
||||||
|
4, 0., 1., 0.
|
||||||
|
*Element, type=S4
|
||||||
|
1, 1, 2, 3, 4
|
||||||
|
*Elset, elset=ShellSet
|
||||||
|
1
|
||||||
|
*Shell Section, elset=ShellSet, material=Steel
|
||||||
|
0.1
|
||||||
|
*End Part
|
||||||
|
*Assembly, name=Assembly
|
||||||
|
*Instance, name=Shell-1, part=ShellPart
|
||||||
|
*End Instance
|
||||||
|
*Nset, nset=All, instance=Shell-1
|
||||||
|
1, 2, 3, 4
|
||||||
|
*End Assembly
|
||||||
|
*Material, name=Steel
|
||||||
|
*Elastic
|
||||||
|
1000., 0.25
|
||||||
|
*Boundary
|
||||||
|
All, 1, 6
|
||||||
|
*Step, name=Step-1, nlgeom=YES
|
||||||
|
*Static
|
||||||
|
0.1, 1., 0.001, 0.1
|
||||||
|
*End Step
|
||||||
|
)inp";
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hdf5File {
|
||||||
|
public:
|
||||||
|
explicit Hdf5File(const std::filesystem::path& path)
|
||||||
|
: handle_{H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT)} {
|
||||||
|
if (handle_ < 0) {
|
||||||
|
throw std::runtime_error{"Unable to open nonlinear results HDF5."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Hdf5File(const Hdf5File&) = delete;
|
||||||
|
Hdf5File& operator=(const Hdf5File&) = delete;
|
||||||
|
|
||||||
|
~Hdf5File() { (void)H5Fclose(handle_); }
|
||||||
|
|
||||||
|
hid_t Get() const noexcept { return handle_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
hid_t handle_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string ReadStringAttribute(const hid_t object, const char* name) {
|
||||||
|
const hid_t attribute = H5Aopen(object, name, H5P_DEFAULT);
|
||||||
|
const hid_t type = attribute < 0 ? -1 : H5Aget_type(attribute);
|
||||||
|
if (attribute < 0 || type < 0) {
|
||||||
|
if (type >= 0) {
|
||||||
|
(void)H5Tclose(type);
|
||||||
|
}
|
||||||
|
if (attribute >= 0) {
|
||||||
|
(void)H5Aclose(attribute);
|
||||||
|
}
|
||||||
|
throw std::runtime_error{"Unable to read nonlinear metadata attribute."};
|
||||||
|
}
|
||||||
|
char* value = nullptr;
|
||||||
|
const herr_t read = H5Aread(attribute, type, &value);
|
||||||
|
const std::string result = read >= 0 && value != nullptr ? value : "";
|
||||||
|
if (value != nullptr) {
|
||||||
|
(void)H5free_memory(value);
|
||||||
|
}
|
||||||
|
(void)H5Tclose(type);
|
||||||
|
(void)H5Aclose(attribute);
|
||||||
|
if (read < 0) {
|
||||||
|
throw std::runtime_error{"Unable to read nonlinear metadata text."};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NSNR-PROC-004, NSNR-PROC-007, and NSNR-PROC-010.
|
||||||
|
TEST(NonlinearStaticProcedure,
|
||||||
|
AllConstrainedNlgeomRunWritesSchemaV1AfterAcceptedFrames) {
|
||||||
|
TempDirectory directory{"schema-v1"};
|
||||||
|
const auto input = directory.Path() / "shellnl.inp";
|
||||||
|
const auto output = directory.Path() / "shellnl_results.h5";
|
||||||
|
WriteText(input, AllConstrainedNonlinearShellDeck());
|
||||||
|
|
||||||
|
testing::internal::CaptureStderr();
|
||||||
|
const int exit_code = fesa::FesaApplication{}.Run({input.string()});
|
||||||
|
const std::string standard_error = testing::internal::GetCapturedStderr();
|
||||||
|
|
||||||
|
ASSERT_EQ(exit_code, 0) << standard_error;
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(output));
|
||||||
|
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
|
||||||
|
const Hdf5File file{output};
|
||||||
|
const hid_t metadata = H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT);
|
||||||
|
ASSERT_GE(metadata, 0);
|
||||||
|
EXPECT_EQ(ReadStringAttribute(metadata, "feature_id"),
|
||||||
|
"nonlinear-static-newton-raphson");
|
||||||
|
(void)H5Gclose(metadata);
|
||||||
|
EXPECT_GT(H5Lexists(file.Get(), "/steps/Step-1/accepted_frames", H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -368,6 +368,20 @@ TEST(LinearStaticCli, DefaultOutputUsesInputSiblingDerivedName) {
|
|||||||
EXPECT_FALSE(std::filesystem::exists(directory.Path() / "results.h5"));
|
EXPECT_FALSE(std::filesystem::exists(directory.Path() / "results.h5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NSNR-PROC-001: log lifecycle begins after leasing and before parse/map.
|
||||||
|
TEST(FesaApplication, OpensDerivedLogBeforeInputMapping) {
|
||||||
|
TempDirectory directory{"log-before-input"};
|
||||||
|
const auto input = directory.Path() / "missing.inp";
|
||||||
|
const auto log = directory.Path() / "missing_log.txt";
|
||||||
|
|
||||||
|
const auto result = RunApplication({input.string()});
|
||||||
|
|
||||||
|
EXPECT_EQ(result.exit_code, 3);
|
||||||
|
ExpectDiagnosticFieldOrder(result.standard_error);
|
||||||
|
ASSERT_TRUE(std::filesystem::exists(log));
|
||||||
|
EXPECT_EQ(std::filesystem::file_size(log), 0U);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
|
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
|
||||||
TempDirectory directory{"exit-codes"};
|
TempDirectory directory{"exit-codes"};
|
||||||
const auto valid_input = directory.Path() / "valid.inp";
|
const auto valid_input = directory.Path() / "valid.inp";
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
|
|||||||
std::sort(generated_names.begin(), generated_names.end());
|
std::sort(generated_names.begin(), generated_names.end());
|
||||||
EXPECT_EQ(generated_names,
|
EXPECT_EQ(generated_names,
|
||||||
(std::vector<std::string>{"cantilever beam.inp",
|
(std::vector<std::string>{"cantilever beam.inp",
|
||||||
|
"cantilever beam_log.txt",
|
||||||
"cantilever beam_results.h5",
|
"cantilever beam_results.h5",
|
||||||
"comparison.json"}));
|
"comparison.json"}));
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ CaseEvidence RunCase(const std::string& case_id,
|
|||||||
EXPECT_EQ(generated,
|
EXPECT_EQ(generated,
|
||||||
(std::vector<std::string>{"comparison.json",
|
(std::vector<std::string>{"comparison.json",
|
||||||
input.filename().string(),
|
input.filename().string(),
|
||||||
|
input.stem().string() + "_log.txt",
|
||||||
results.filename().string()}));
|
results.filename().string()}));
|
||||||
EXPECT_TRUE(std::filesystem::is_directory(reference_directory));
|
EXPECT_TRUE(std::filesystem::is_directory(reference_directory));
|
||||||
ExpectUnchanged(input, input_before);
|
ExpectUnchanged(input, input_before);
|
||||||
|
|||||||
Reference in New Issue
Block a user