diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index c546c2822..64608b6b1 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -178,6 +178,8 @@ set(CSRCS eq_assem.h eq_assem.cpp fluid.h fluid.cpp fsi.h fsi.cpp + fsi_coupling.h fsi_coupling.cpp + PartitionedFSI.h PartitionedFSI.cpp fs.h fs.cpp fft.h fft.cpp heatf.h heatf.cpp @@ -357,7 +359,13 @@ if(ENABLE_UNIT_TEST) # include source files (same as what svMultiPhysics does except for main.cpp) add_executable(run_all_unit_tests ${CSRCS}) - + + # Define test data directory for integration tests + # CMAKE_SOURCE_DIR points to Code/, so go up one level to reach the repo root + target_compile_definitions(run_all_unit_tests PRIVATE + TEST_DATA_DIR="${CMAKE_SOURCE_DIR}/../tests/cases" + ) + if(USE_TRILINOS) target_link_libraries(run_all_unit_tests ${Trilinos_LIBRARIES} ${Trilinos_TPL_LIBRARIES}) endif() diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index 26410a084..3842cb077 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -1553,9 +1553,14 @@ class ComMod { /// @brief Whether there is a requirement to update mesh and Dn-Do variables bool dFlag = false; - /// @brief Whether mesh is moving + /// @brief Whether mesh is moving (monolithic FSI: mesh velocity in DOFs nsd+1..2*nsd) bool mvMsh = false; + /// @brief ALE mesh velocity for partitioned FSI (nsd, tnNo). + /// When non-empty, the fluid assembly subtracts this from the convective + /// velocity, providing the ALE correction without expanding tDof. + Array ale_mesh_velocity; + /// @brief Whether to averaged results bool saveAve = false; diff --git a/Code/Source/solver/Integrator.cpp b/Code/Source/solver/Integrator.cpp index 90dddb1d1..3d5b94d0a 100644 --- a/Code/Source/solver/Integrator.cpp +++ b/Code/Source/solver/Integrator.cpp @@ -17,6 +17,7 @@ #include "utils.h" #include +#include #include #include @@ -171,6 +172,94 @@ bool Integrator::step() { } // End of Newton iteration loop } +//------------------------ +// step_equation +//------------------------ +/// @brief Solve a single equation to convergence without cycling to other equations +bool Integrator::step_equation(int iEq, std::function post_assembly) { + using namespace consts; + + auto& com_mod = simulation_->com_mod; + auto& cm_mod = simulation_->cm_mod; + + int& cTS = com_mod.cTS; + + // Set up for this equation + com_mod.cEq = iEq; + auto& eq = com_mod.eq[iEq]; + eq.itr = 0; + eq.ok = false; + eq.iNorm = 0.0; + + newton_count_ = 1; + + while (true) { + istr_ = "_" + std::to_string(cTS) + "_" + std::to_string(newton_count_); + auto& eq = com_mod.eq[iEq]; + + // Coupled BC handling (same as step()) + if (com_mod.cplBC.coupled && iEq == 0) { + set_bc::set_bc_cpl(com_mod, cm_mod, solutions_); + set_bc::set_bc_dir(com_mod, solutions_); + } + + // Initiator step for Generalized α-Method + initiator_step(); + + if (com_mod.Rd.size() != 0) { + com_mod.Rd = 0.0; + com_mod.Kd = 0.0; + } + + // Allocate, assemble, apply BCs + allocate_linear_system(eq); + set_body_forces(); + assemble_equations(); + apply_boundary_conditions(); + + // Optional post-assembly callback (e.g., for injecting interface traction) + if (post_assembly) { + post_assembly(); + } + + // Synchronize R across processes + if (!eq.assmTLS) { + all_fun::commu(com_mod, com_mod.R); + } + + // Update residual in displacement equation for USTRUCT phys + if (com_mod.sstEq) { + ustruct::ustruct_r(com_mod, solutions_); + } + + // Set the residual of the continuity equation to 0 on edge nodes + if (std::set{Equation_stokes, Equation_fluid, Equation_ustruct, Equation_FSI}.count(eq.phys) != 0) { + fs::thood_val_rc(com_mod); + } + + set_bc::set_bc_undef_neu(com_mod); + update_residual_arrays(eq); + + // Solve linear system + solve_linear_system(); + + // Update solution and check convergence (no equation cycling) + update_solution(); + + if (eq.ok) { + return true; + } + + // Abort on NaN in residual norm (indicates divergence). + if (newton_count_ > 1 && std::isnan(eq.FSILS.RI.iNorm)) { + return false; + } + + output::output_result(simulation_, com_mod.timeP, 2, iEq); + newton_count_ += 1; + } +} + //------------------------ // initiator_step //------------------------ @@ -374,8 +463,8 @@ void Integrator::update_residual_arrays(eqType& eq) { // 1. Bazilevs, et al. "Isogeometric fluid-structure interaction: // theory, algorithms, and computations.", Computational Mechanics, // 43 (2008): 3-37. doi: 10.1007/s00466-008-0315-x -// 2. Bazilevs, et al. "Variational multiscale residual-based -// turbulence modeling for large eddy simulation of incompressible +// 2. Bazilevs, et al. "Variational multiscale residual-based +// turbulence modeling for large eddy simulation of incompressible // flows.", CMAME (2007) //------------------------ // predictor (picp) @@ -621,11 +710,13 @@ void Integrator::initiator(SolutionStates& solutions) } } //------------------------ -// corrector +// update_solution //------------------------ -/// @brief Corrector with convergence check +/// @brief Update solution from linear solve result and check convergence /// -/// Decision for next eqn is also made here (modifies cEq global). +/// Performs the corrector update of An, Yn, Dn from the Newton solve result +/// and checks convergence norms. Sets eq.ok if converged. +/// Does NOT handle equation cycling (that is done by corrector()). /// /// Modifies: /// \code {.cpp} @@ -637,13 +728,12 @@ void Integrator::initiator(SolutionStates& solutions) /// com_mod.pS0 /// com_mod.pSa /// com_mod.pSn -/// -/// com_mod.cEq /// eq.FSILS.RI.iNorm /// eq.pNorm +/// eq.ok /// \endcode // -void Integrator::corrector() +void Integrator::update_solution() { using namespace consts; @@ -853,10 +943,42 @@ void Integrator::corrector() dmsg << "com_mod.eq[1].ok: " << com_mod.eq[1].ok; #endif } +} + +//------------------------ +// corrector +//------------------------ +/// @brief Corrector with convergence check and equation cycling +/// +/// Calls update_solution() to update the solution and check convergence, +/// then handles equation switching for coupled problems (modifies cEq global). +// +void Integrator::corrector() +{ + using namespace consts; + auto& com_mod = simulation_->com_mod; + const int tnNo = com_mod.tnNo; + auto& cEq = com_mod.cEq; + auto& eq = com_mod.eq[cEq]; + + auto& An = solutions_.current.get_acceleration(); + auto& Dn = solutions_.current.get_displacement(); + auto& Yn = solutions_.current.get_velocity(); + + #define n_debug_corrector_cycling + #ifdef debug_corrector_cycling + DebugMsg dmsg(__func__, com_mod.cm.idcm()); + dmsg.banner(); + #endif + + // Update solution and check convergence + update_solution(); + + // Check if all equations converged - if so, skip equation cycling auto& eqs = com_mod.eq; if (std::count_if(eqs.begin(),eqs.end(),[](eqType& eq){return eq.ok;}) == eqs.size()) { - #ifdef debug_corrector + #ifdef debug_corrector_cycling dmsg << "all ok"; #endif return; @@ -868,7 +990,7 @@ void Integrator::corrector() // For coupled equations, if explicit geometric coupling is not used, // increment the equation counter after each linear solve cEq = cEq + 1; - #ifdef debug_corrector + #ifdef debug_corrector_cycling dmsg << "eq " << " coupled "; dmsg << "1st update cEq: " << cEq; #endif @@ -929,7 +1051,7 @@ void Integrator::corrector() cEq = cEq + 1; } } - #ifdef debug_corrector + #ifdef debug_corrector_cycling dmsg << "eq " << " coupled "; dmsg << "2nd update cEq: " << cEq; #endif diff --git a/Code/Source/solver/Integrator.h b/Code/Source/solver/Integrator.h index 790e17eff..23570889b 100644 --- a/Code/Source/solver/Integrator.h +++ b/Code/Source/solver/Integrator.h @@ -9,6 +9,38 @@ #include "Vector.h" #include "Simulation.h" +#include + +/// @brief Newmark time integration utilities. +/// +/// Compute consistent state variables from a prescribed displacement or +/// velocity using the Newmark-beta / generalized-alpha relationships: +/// Dn = Do + dt*Yo + dt^2*((0.5-beta)*Ao + beta*An) +/// Yn = Yo + dt*((1-gamma)*Ao + gamma*An) +namespace newmark { + +/// Compute acceleration and velocity from prescribed displacement. +inline void state_from_displacement( + double d_new, double d_old, double v_old, double a_old, + double dt, double beta, double gam, + double& a_new, double& v_new) +{ + a_new = (d_new - d_old - dt * v_old) / (beta * dt * dt) + - (0.5 - beta) / beta * a_old; + v_new = v_old + dt * ((1.0 - gam) * a_old + gam * a_new); +} + +/// Compute acceleration from prescribed velocity. +inline void state_from_velocity( + double v_new, double v_old, double a_old, + double dt, double gam, + double& a_new) +{ + a_new = (v_new - v_old) / (gam * dt) - (1.0 - gam) / gam * a_old; +} + +} // namespace newmark + /** * @brief Integrator class encapsulates the Newton iteration loop for time integration * @@ -42,6 +74,21 @@ class Integrator { */ bool step(); + /** + * @brief Solve a single equation to convergence (for partitioned coupling) + * + * Runs Newton iterations for only the specified equation, without cycling + * to other equations. Used by partitioned FSI to solve fluid, solid, and + * mesh equations independently. + * + * @param iEq Index of the equation to solve + * @param post_assembly Optional callback invoked after boundary condition + * application but before the linear solve. Used by partitioned FSI + * to inject interface traction into the residual. + * @return True if the equation converged, false if max iterations reached + */ + bool step_equation(int iEq, std::function post_assembly = nullptr); + /** * @brief Perform predictor step for next time step * @@ -173,11 +220,20 @@ class Integrator { */ void initiator(SolutionStates& solutions); + /** + * @brief Update solution and check convergence of current equation + * + * Performs the corrector step: updates An, Yn, Dn from the linear solve + * result and checks convergence norms. Sets eq.ok if converged. + * Does NOT handle equation cycling -- that is done separately in corrector(). + */ + void update_solution(); + /** * @brief Corrector function with convergence check (corrector) * - * Updates solution at n+1 time level and checks convergence of Newton - * iterations. Also handles equation switching for coupled problems. + * Calls update_solution(), then handles equation switching for coupled + * problems (modifies cEq global). */ void corrector(); diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index fda5af63b..64a087086 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -248,6 +248,9 @@ void Parameters::read_xml(std::string file_name) // Set mesh projection parameters. set_projection_values(root_element); + // Set partitioned coupling parameters. + set_partitioned_coupling_values(root_element); + // Set Add_equation values. set_equation_values(root_element); @@ -278,6 +281,8 @@ void Parameters::set_equation_values(tinyxml2::XMLElement* root_element) auto eq_params = new EquationParameters(); eq_params->type.set(std::string(eq_type)); + const char* eq_role = add_eq_item->Attribute("role"); + if (eq_role) eq_params->role.set(std::string(eq_role)); eq_params->set_values(add_eq_item); equation_parameters.push_back(eq_params); @@ -2926,6 +2931,50 @@ void ProjectionParameters::set_values(tinyxml2::XMLElement* xml_elem) xml_util_set_parameters(ftpr, xml_elem, error_msg); } +////////////////////////////////////////////////////////// +// PartitionedCouplingParameters // +////////////////////////////////////////////////////////// + +const std::string PartitionedCouplingParameters::xml_element_name_ = "Partitioned_coupling"; + +PartitionedCouplingParameters::PartitionedCouplingParameters() +{ + bool required = true; + + set_parameter("Max_coupling_iterations", 50, !required, max_coupling_iterations); + set_parameter("Coupling_tolerance", 1e-6, !required, coupling_tolerance); + set_parameter("Initial_relaxation", 1.0, !required, initial_relaxation); + set_parameter("Omega_max", 1.0, !required, omega_max); + set_parameter("Coupling_method", "aitken", !required, coupling_method); + set_parameter("Fluid_interface_face", "", required, fluid_interface_face); + set_parameter("Solid_interface_face", "", required, solid_interface_face); + +} + +void PartitionedCouplingParameters::set_values(tinyxml2::XMLElement* xml_elem) +{ + using namespace tinyxml2; + std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; + + using std::placeholders::_1; + using std::placeholders::_2; + + std::function ftpr = + std::bind(&PartitionedCouplingParameters::set_parameter_value, *this, _1, _2); + + xml_util_set_parameters(ftpr, xml_elem, error_msg); + value_set = true; +} + +void Parameters::set_partitioned_coupling_values(tinyxml2::XMLElement* root_element) +{ + auto item = root_element->FirstChildElement(PartitionedCouplingParameters::xml_element_name_.c_str()); + if (item == nullptr) { + return; + } + partitioned_coupling_parameters.set_values(item); +} + ////////////////////////////////////////////////////////// // RISProjectionParameters // ////////////////////////////////////////////////////////// diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 1e679ab5f..ef0ddc905 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -1508,6 +1508,7 @@ class EquationParameters : public ParameterLists Parameter tolerance; Parameter type; + Parameter role; // "partitioned_fluid", "partitioned_solid", or "partitioned_mesh" Parameter use_taylor_hood_type_basis; // Explicit geometric coupling for FSI simulations: the fluid-structure equations @@ -1792,6 +1793,34 @@ class URISMeshParameters : public ParameterLists +////////////////////////////////////////////////////////// +// PartitionedCouplingParameters // +////////////////////////////////////////////////////////// + +/// @brief Parameters for the 'Partitioned_coupling' XML element. +/// +/// Configures partitioned FSI coupling between separately solved +/// fluid and solid equations with Aitken relaxation. +class PartitionedCouplingParameters : public ParameterLists +{ + public: + PartitionedCouplingParameters(); + static const std::string xml_element_name_; + void set_values(tinyxml2::XMLElement* xml_elem); + bool defined() const { return value_set; } + + Parameter max_coupling_iterations; + Parameter coupling_tolerance; + Parameter initial_relaxation; + Parameter omega_max; + Parameter coupling_method; // "constant" or "aitken" + Parameter fluid_interface_face; + Parameter solid_interface_face; + + bool value_set = false; +}; + + /// @brief The Parameters class stores parameter values read in from a solver input file. class Parameters { @@ -1815,6 +1844,7 @@ class Parameters { void set_RIS_projection_values(tinyxml2::XMLElement* root_element); void set_URIS_mesh_values(tinyxml2::XMLElement* root_element); + void set_partitioned_coupling_values(tinyxml2::XMLElement* root_element); // Objects representing each parameter section of XML file. ContactParameters contact_parameters; @@ -1827,6 +1857,8 @@ class Parameters { std::vector RIS_projection_parameters; std::vector URIS_mesh_parameters; + PartitionedCouplingParameters partitioned_coupling_parameters; + }; #endif diff --git a/Code/Source/solver/PartitionedFSI.cpp b/Code/Source/solver/PartitionedFSI.cpp new file mode 100644 index 000000000..0f7c57faf --- /dev/null +++ b/Code/Source/solver/PartitionedFSI.cpp @@ -0,0 +1,959 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "PartitionedFSI.h" +#include "Integrator.h" +#include "fsi_coupling.h" +#include "post.h" +#include "set_bc.h" +#include "distribute.h" +#include "initialize.h" +#include "output.h" +#include "vtk_xml.h" +#include "read_files.h" + +#include +#include +#include +#include +#include + +/// Check if any value in the solution arrays is NaN +static bool has_nan(const SolutionStates& sol) { + const Array* arrays[] = { + &sol.current.get_velocity(), + &sol.current.get_acceleration(), + &sol.current.get_displacement() + }; + for (auto* arr : arrays) + for (int a = 0; a < arr->ncols(); a++) + for (int i = 0; i < arr->nrows(); i++) + if (std::isnan((*arr)(i, a))) return true; + return false; +} + + +//---------------------------------------------------------------------- +// Helper: initialize one sub-simulation through the standard pipeline +//---------------------------------------------------------------------- +static void init_sub_sim(Simulation* sim, const std::string& xml_path) +{ + read_files_ns::read_files(sim, xml_path); + sim->logger.set_cout_write(false); + + // The mesh sub-sim includes a stub so that read_files + // accepts the mesh equation, but this sim has only the mesh equation (tDof=3). + // baf_ini assumes FSI DOF layout (Do(i+nsd+1,Ac) reaches row 4) when mvMsh=true, + // which would go out-of-bounds for the standalone mesh sub-sim. + if (sim->com_mod.nEq == 1 && + sim->com_mod.eq[0].phys == consts::EquationType::phys_mesh) { + sim->com_mod.mvMsh = false; + } + + distribute(sim); + Vector init_time(3); + initialize(sim, init_time); + for (int iEq = 0; iEq < sim->com_mod.nEq; iEq++) { + add_eq_linear_algebra(sim->com_mod, sim->com_mod.eq[iEq]); + } +} + +//---------------------------------------------------------------------- +// build_sub_xml — extract one role's equation + its meshes from the +// main XML and write a minimal standalone sub-simulation XML to a +// temp file. Returns the temp file path. +// +// Mesh association: the meshes included in the sub-XML are those whose +// value matches any child of the target +// equation. The partitioned_mesh role falls back to the fluid meshes +// when the mesh equation carries no explicit domain block. +//---------------------------------------------------------------------- +std::string PartitionedFSI::build_sub_xml(const std::string& main_xml_path, + const std::string& role) +{ + using namespace tinyxml2; + + XMLDocument doc; + if (doc.LoadFile(main_xml_path.c_str()) != XML_SUCCESS) + throw std::runtime_error("[PartitionedFSI] Cannot parse main XML: " + main_xml_path); + + XMLElement* root = doc.FirstChildElement("svMultiPhysicsFile"); + if (!root) + throw std::runtime_error("[PartitionedFSI] Missing root in " + main_xml_path); + + // Find the equation element with the requested role attribute. + XMLElement* target_eq = nullptr; + for (XMLElement* eq = root->FirstChildElement("Add_equation"); eq; + eq = eq->NextSiblingElement("Add_equation")) { + const char* r = eq->Attribute("role"); + if (r && std::string(r) == role) { target_eq = eq; break; } + } + if (!target_eq) + throw std::runtime_error("[PartitionedFSI] No found in " + main_xml_path); + + // Collect domain IDs used by the target equation. + std::set domain_ids; + for (XMLElement* d = target_eq->FirstChildElement("Domain"); d; + d = d->NextSiblingElement("Domain")) + domain_ids.insert(d->IntAttribute("id", -1)); + + // Mesh role with no domain block: use the same domains as the fluid equation. + if (domain_ids.empty() && role == "partitioned_mesh") { + for (XMLElement* eq = root->FirstChildElement("Add_equation"); eq; + eq = eq->NextSiblingElement("Add_equation")) { + const char* r = eq->Attribute("role"); + if (r && std::string(r) == "partitioned_fluid") { + for (XMLElement* d = eq->FirstChildElement("Domain"); d; + d = d->NextSiblingElement("Domain")) + domain_ids.insert(d->IntAttribute("id", -1)); + break; + } + } + } + + // Build sub-document. + XMLDocument sub; + XMLElement* sub_root = sub.NewElement("svMultiPhysicsFile"); + sub_root->SetAttribute("version", "0.1"); + sub.InsertFirstChild(sub_root); + + // Copy GeneralSimulationParameters, overriding the VTK save prefix + // so each sub-sim writes to distinct files (result_fluid_*, result_solid_*, result_mesh_*). + XMLElement* gen = root->FirstChildElement("GeneralSimulationParameters"); + if (gen) { + XMLElement* gen_clone = gen->DeepClone(&sub)->ToElement(); + // Map role → short suffix used in the prefix name + std::string suffix; + if (role == "partitioned_fluid") suffix = "fluid"; + else if (role == "partitioned_solid") suffix = "solid"; + else if (role == "partitioned_mesh") suffix = "mesh"; + if (!suffix.empty()) { + XMLElement* name_elem = gen_clone->FirstChildElement("Name_prefix_of_saved_VTK_files"); + if (name_elem) { + std::string base_prefix = name_elem->GetText() ? name_elem->GetText() : "result"; + // trim whitespace + base_prefix.erase(0, base_prefix.find_first_not_of(" \t\n\r")); + base_prefix.erase(base_prefix.find_last_not_of(" \t\n\r") + 1); + name_elem->SetText((" " + base_prefix + "_" + suffix + " ").c_str()); + } + } + sub_root->InsertEndChild(gen_clone); + } + + // Copy matching Add_mesh elements. + for (XMLElement* mesh = root->FirstChildElement("Add_mesh"); mesh; + mesh = mesh->NextSiblingElement("Add_mesh")) { + XMLElement* dom_elem = mesh->FirstChildElement("Domain"); + if (!dom_elem) continue; + int mesh_dom = dom_elem->IntText(-1); + if (domain_ids.empty() || domain_ids.count(mesh_dom)) + sub_root->InsertEndChild(mesh->DeepClone(&sub)); + } + + // Clone the equation, stripping the role attribute. + XMLElement* eq_clone = target_eq->DeepClone(&sub)->ToElement(); + eq_clone->DeleteAttribute("role"); + sub_root->InsertEndChild(eq_clone); + + // The mesh sub-sim needs a minimal block so that + // read_files sets mvMsh=true (required for the mesh equation to be valid). + if (role == "partitioned_mesh") { + XMLElement* pcp = sub.NewElement("Partitioned_coupling"); + XMLElement* fface = sub.NewElement("Fluid_interface_face"); + fface->SetText("dummy"); + XMLElement* sface = sub.NewElement("Solid_interface_face"); + sface->SetText("dummy"); + pcp->InsertEndChild(fface); + pcp->InsertEndChild(sface); + sub_root->InsertEndChild(pcp); + } + + // Write to temp file alongside the main XML. + std::string base = main_xml_path; + auto slash = base.find_last_of('/'); + std::string dir = (slash != std::string::npos) ? base.substr(0, slash + 1) : "./"; + std::string temp_path = dir + ".partfsi_" + role + "_tmp.xml"; + if (sub.SaveFile(temp_path.c_str()) != XML_SUCCESS) + throw std::runtime_error("[PartitionedFSI] Cannot write temp sub-XML: " + temp_path); + return temp_path; +} + +//---------------------------------------------------------------------- +// Constructor +//---------------------------------------------------------------------- +PartitionedFSI::PartitionedFSI(Simulation* main_simulation, + const PartitionedFSIConfig& config, + const std::string& xml_file_path) + : main_sim_(main_simulation), config_(config), + xml_file_path_(xml_file_path), omega_(config.initial_relaxation) +{ + auto& cm = main_sim_->com_mod.cm; + auto& cm_mod = main_sim_->cm_mod; + + // Build per-role sub-XMLs from the main XML (all ranks write identical content). + std::string fluid_xml = build_sub_xml(xml_file_path_, "partitioned_fluid"); + std::string solid_xml = build_sub_xml(xml_file_path_, "partitioned_solid"); + std::string mesh_xml = build_sub_xml(xml_file_path_, "partitioned_mesh"); + temp_xml_paths_ = {fluid_xml, solid_xml, mesh_xml}; + + // 3 separate sub-sims: fluid, solid, mesh + fluid_sim_ = std::make_unique(); + init_sub_sim(fluid_sim_.get(), fluid_xml); + + solid_sim_ = std::make_unique(); + init_sub_sim(solid_sim_.get(), solid_xml); + + mesh_sim_ = std::make_unique(); + init_sub_sim(mesh_sim_.get(), mesh_xml); + + + if (cm.mas(cm_mod)) { + // Open log files + std::string log_dir = fluid_sim_->get_chnl_mod().appPath; + coupling_log_.open(log_dir + "coupling.dat"); + char hdr[256]; + snprintf(hdr, sizeof(hdr), "# %4s %3s %10s %5s %10s %10s %10s %10s", + "cTS", "cp", "time", "dB", "Ri/R1", "Ri/R0", "omega", "|disp|"); + coupling_log_ << hdr << std::endl; + + histor_log_.open(log_dir + "histor.dat"); + } + + resolve_faces(); + build_node_maps(); +} + +PartitionedFSI::~PartitionedFSI() +{ + for (const auto& p : temp_xml_paths_) std::remove(p.c_str()); +} + +//---------------------------------------------------------------------- +// resolve_faces +//---------------------------------------------------------------------- +void PartitionedFSI::resolve_faces() +{ + auto find_face = [](Simulation* sim, const std::string& face_name, + const faceType*& face_out, const mshType*& mesh_out) { + for (int iM = 0; iM < sim->com_mod.nMsh; iM++) { + auto& msh = sim->com_mod.msh[iM]; + for (int iFa = 0; iFa < msh.nFa; iFa++) { + if (msh.fa[iFa].name == face_name) { + face_out = &msh.fa[iFa]; + mesh_out = &msh; + return; + } + } + } + throw std::runtime_error("[PartitionedFSI] Face '" + face_name + "' not found."); + }; + + find_face(fluid_sim_.get(), config_.fluid_interface_face, fluid_face_, fluid_mesh_); + find_face(solid_sim_.get(), config_.solid_interface_face, solid_face_, solid_mesh_); + find_face(mesh_sim_.get(), config_.fluid_interface_face, mesh_face_, mesh_mesh_); +} + +//---------------------------------------------------------------------- +// compute_face_global_info — gather per-rank nNo to compute global nNo +// and this rank's offset within the global face node ordering. +//---------------------------------------------------------------------- +void PartitionedFSI::compute_face_global_info( + const faceType& face, cmType& cm, const CmMod& cm_mod, + int& global_nNo, int& local_offset) +{ + int np = cm.np(); + int my_rank = cm.id(); + int local_nNo = face.nNo; + + std::vector all_nNo(np); + MPI_Allgather(&local_nNo, 1, MPI_INT, + all_nNo.data(), 1, MPI_INT, cm.com()); + + global_nNo = 0; + local_offset = 0; + for (int p = 0; p < np; p++) { + if (p < my_rank) local_offset += all_nNo[p]; + global_nNo += all_nNo[p]; + } +} + +//---------------------------------------------------------------------- +// gather_face_data — MPI_Allgatherv local face data to all ranks. +// local_data is (nrows, local_nNo); returns (nrows, global_nNo). +// Rank ordering in global array matches the local_offset convention. +//---------------------------------------------------------------------- +Array PartitionedFSI::gather_face_data( + const Array& local_data, + int global_nNo, int /*local_offset*/, + cmType& cm, const CmMod& cm_mod) +{ + const int nrows = local_data.nrows(); + const int local_nNo = local_data.ncols(); + const int np = cm.np(); + + // Pack as flat row-major: [node0_row0, node0_row1, ..., node1_row0, ...] + std::vector local_flat(nrows * local_nNo); + for (int a = 0; a < local_nNo; a++) + for (int i = 0; i < nrows; i++) + local_flat[a * nrows + i] = local_data(i, a); + + int send_count = nrows * local_nNo; + std::vector recv_counts(np), displs(np); + MPI_Allgather(&send_count, 1, MPI_INT, + recv_counts.data(), 1, MPI_INT, cm.com()); + int total = 0; + for (int p = 0; p < np; p++) { displs[p] = total; total += recv_counts[p]; } + + std::vector global_flat(total); + MPI_Allgatherv(local_flat.data(), send_count, MPI_DOUBLE, + global_flat.data(), recv_counts.data(), displs.data(), + MPI_DOUBLE, cm.com()); + + Array result(nrows, global_nNo); + int node_offset = 0; + for (int p = 0; p < np; p++) { + int p_nNo = recv_counts[p] / nrows; + for (int la = 0; la < p_nNo; la++) + for (int i = 0; i < nrows; i++) + result(i, node_offset + la) = global_flat[displs[p] + la * nrows + i]; + node_offset += p_nNo; + } + return result; +} + +//---------------------------------------------------------------------- +// gather_global_map — all-gather a local (local_src → global_tgt) map +// into a global (global_src → global_tgt) map. +//---------------------------------------------------------------------- +void PartitionedFSI::gather_global_map( + const std::vector& local_map, + int global_src_nNo, + cmType& cm, const CmMod& cm_mod, + std::vector& global_map) +{ + int np = cm.np(); + int send_count = static_cast(local_map.size()); + + std::vector recv_counts(np), displs(np); + MPI_Allgather(&send_count, 1, MPI_INT, + recv_counts.data(), 1, MPI_INT, cm.com()); + int total = 0; + for (int p = 0; p < np; p++) { displs[p] = total; total += recv_counts[p]; } + + std::vector global_flat(total); + MPI_Allgatherv(local_map.data(), send_count, MPI_INT, + global_flat.data(), recv_counts.data(), displs.data(), + MPI_INT, cm.com()); + + global_map.assign(global_src_nNo, -1); + int offset = 0; + for (int p = 0; p < np; p++) { + for (int la = 0; la < recv_counts[p]; la++) + global_map[offset + la] = global_flat[displs[p] + la]; + offset += recv_counts[p]; + } +} + +//---------------------------------------------------------------------- +// build_face_node_map — match each LOCAL face_a node to its nearest +// node in the PRE-GATHERED global face_b coordinates. +// Returns local_src_idx → global_tgt_idx map. +//---------------------------------------------------------------------- +void PartitionedFSI::build_face_node_map( + const faceType& face_a, const ComMod& com_a, + int global_b_nNo, const Array& global_b_coords, + std::vector& a_to_global_b) +{ + const int nsd = com_a.nsd; + const double tol = 1e-8; + a_to_global_b.assign(face_a.nNo, -1); + + for (int a = 0; a < face_a.nNo; a++) { + int Ac = face_a.gN(a); + double best = 1e30; + int best_b = -1; + for (int bg = 0; bg < global_b_nNo; bg++) { + double d2 = 0.0; + for (int i = 0; i < nsd; i++) { + double d = com_a.x(i, Ac) - global_b_coords(i, bg); + d2 += d * d; + } + if (d2 < best) { best = d2; best_b = bg; } + } + if (best < tol * tol) a_to_global_b[a] = best_b; + } +} + +//---------------------------------------------------------------------- +// build_node_maps — build global→global interface node maps. +// Each sub-mesh is independently distributed, so we gather all face +// coordinates from all ranks before performing the nearest-neighbor +// search; then gather the local partial maps into global maps. +//---------------------------------------------------------------------- +void PartitionedFSI::build_node_maps() +{ + auto& cm = main_sim_->com_mod.cm; + auto& cm_mod = main_sim_->cm_mod; + const int nsd = main_sim_->com_mod.nsd; + + // Compute global nNo and per-rank offsets for each interface face + compute_face_global_info(*solid_face_, cm, cm_mod, + solid_face_global_nNo_, solid_face_local_offset_); + compute_face_global_info(*fluid_face_, cm, cm_mod, + fluid_face_global_nNo_, fluid_face_local_offset_); + compute_face_global_info(*mesh_face_, cm, cm_mod, + mesh_face_global_nNo_, mesh_face_local_offset_); + + // Gather face coordinates globally for each sub-mesh face + auto pack_coords = [&](const faceType& face, const ComMod& com) { + Array local(nsd, face.nNo); + for (int a = 0; a < face.nNo; a++) { + int Ac = face.gN(a); + for (int i = 0; i < nsd; i++) + local(i, a) = com.x(i, Ac); + } + return local; + }; + + auto global_solid_coords = gather_face_data( + pack_coords(*solid_face_, solid_sim_->com_mod), + solid_face_global_nNo_, solid_face_local_offset_, cm, cm_mod); + auto global_fluid_coords = gather_face_data( + pack_coords(*fluid_face_, fluid_sim_->com_mod), + fluid_face_global_nNo_, fluid_face_local_offset_, cm, cm_mod); + auto global_mesh_coords = gather_face_data( + pack_coords(*mesh_face_, mesh_sim_->com_mod), + mesh_face_global_nNo_, mesh_face_local_offset_, cm, cm_mod); + + // Build local (local_src → global_tgt) maps, then gather to global maps + std::vector local_s2f, local_f2s, local_s2m; + build_face_node_map(*solid_face_, solid_sim_->com_mod, + fluid_face_global_nNo_, global_fluid_coords, local_s2f); + build_face_node_map(*fluid_face_, fluid_sim_->com_mod, + solid_face_global_nNo_, global_solid_coords, local_f2s); + build_face_node_map(*solid_face_, solid_sim_->com_mod, + mesh_face_global_nNo_, global_mesh_coords, local_s2m); + + gather_global_map(local_s2f, solid_face_global_nNo_, cm, cm_mod, solid_to_fluid_map_); + gather_global_map(local_f2s, fluid_face_global_nNo_, cm, cm_mod, fluid_to_solid_map_); + gather_global_map(local_s2m, solid_face_global_nNo_, cm, cm_mod, solid_to_mesh_map_); +} + +//---------------------------------------------------------------------- +// transfer_data +//---------------------------------------------------------------------- +Array PartitionedFSI::transfer_data( + const std::vector& src_to_tgt_map, + const Array& src_data, int tgt_nNo) +{ + int nrows = src_data.nrows(); + Array result(nrows, tgt_nNo); + for (int a = 0; a < static_cast(src_to_tgt_map.size()); a++) { + int b = src_to_tgt_map[a]; + if (b >= 0) { + for (int i = 0; i < nrows; i++) result(i, b) = src_data(i, a); + } + } + return result; +} + +//---------------------------------------------------------------------- +// relax_interface — updates disp_prev_ and vel_prev_ +//---------------------------------------------------------------------- +void PartitionedFSI::relax_interface(int cp, int nsd, + const Array& disp_current) +{ + switch (config_.coupling_method) { + case CouplingMethod::constant: + relax_constant(cp, nsd, disp_current); + break; + case CouplingMethod::aitken: + relax_aitken(cp, nsd, disp_current); + break; + } +} + +//---------------------------------------------------------------------- +// relax_constant — fixed relaxation (operates on global face arrays) +//---------------------------------------------------------------------- +void PartitionedFSI::relax_constant(int cp, int nsd, + const Array& disp_current) +{ + omega_ = config_.initial_relaxation; + for (int a = 0; a < solid_face_global_nNo_; a++) + for (int i = 0; i < nsd; i++) + disp_prev_(i, a) += omega_ * (disp_current(i, a) - disp_prev_(i, a)); +} + +//---------------------------------------------------------------------- +// relax_aitken — Aitken Delta^2 (Küttler & Wall 2008, Eq. 44) +// Operates on global face arrays; all ranks have identical data so no +// MPI reduction is needed here. +//---------------------------------------------------------------------- +void PartitionedFSI::relax_aitken(int cp, int nsd, + const Array& disp_current) +{ + const int u = nsd * solid_face_global_nNo_; + + // Build residual r = x_tilde - x + std::vector r(u); + for (int a = 0; a < solid_face_global_nNo_; a++) + for (int i = 0; i < nsd; i++) + r[a * nsd + i] = disp_current(i, a) - disp_prev_(i, a); + + // Aitken update: omega = -omega * r^T (r_new - r_old) / |r_new - r_old|^2 + // Negative omega allowed (corrects overshoot) + if (cp > 0 && !r_prev_.empty()) { + double num = 0, den = 0; + for (int j = 0; j < u; j++) { + double dr = r[j] - r_prev_[j]; + num += r_prev_[j] * dr; + den += dr * dr; + } + if (den > 1e-30) { + omega_ = -omega_ * num / den; + if (std::abs(omega_) > config_.omega_max) + omega_ = (omega_ > 0) ? config_.omega_max : -config_.omega_max; + } + } + r_prev_ = r; + + // Apply: x_{k+1} = x_k + omega * r + for (int a = 0; a < solid_face_global_nNo_; a++) + for (int i = 0; i < nsd; i++) + disp_prev_(i, a) += omega_ * (disp_current(i, a) - disp_prev_(i, a)); +} + +//====================================================================== +// run — full time-stepping loop with Dirichlet-Neumann coupling +//====================================================================== +void PartitionedFSI::run() +{ + auto& main_com = main_sim_->com_mod; + auto& cm_mod = main_sim_->cm_mod; + auto& cm = main_com.cm; + + int nTS = main_com.nTS; + int& cTS = main_com.cTS; + double& dt = main_com.dt; + double& time = main_com.time; + int nITs = main_com.nITs; + + if (cTS <= nITs) dt = dt / 10.0; + + Simulation* sims[3] = {fluid_sim_.get(), solid_sim_.get(), mesh_sim_.get()}; + + while (true) { + if (cTS == nITs) dt = 10.0 * dt; + cTS = cTS + 1; + time = time + dt; + + // Sync time to sub-sims + for (auto* sim : sims) { + sim->com_mod.cTS = cTS; + sim->com_mod.time = time; + sim->com_mod.dt = dt; + for (auto& eq : sim->com_mod.eq) { eq.itr = 0; eq.ok = false; } + } + + if (cm.mas(cm_mod)) { + if (histor_log_.is_open()) { + histor_log_ << std::string(70, '=') << std::endl; + histor_log_ << " TIME STEP " << cTS << " t=" << time << " dt=" << dt << std::endl; + histor_log_ << std::string(70, '=') << std::endl; + } + } + + // Predictor + Dirichlet BCs for each sub-sim + for (auto* sim : sims) { + sim->get_integrator().predictor(); + set_bc::set_bc_dir(sim->com_mod, sim->get_integrator().get_solutions()); + } + + // Coupling loop + bool converged = step(); + + if (!converged && cm.mas(cm_mod)) { + std::cout << " TIME STEP " << cTS << " FAILED (NaN or no convergence)" << std::endl; + if (histor_log_.is_open()) + histor_log_ << " TIME STEP " << cTS << " FAILED (NaN or no convergence)" << std::endl; + } + + // Stop on failure + if (!converged) break; + + // Save results + save_results(); + + // Copy current -> old + for (auto* sim : sims) { + auto& sol = sim->get_integrator().get_solutions(); + sol.old.get_acceleration() = sol.current.get_acceleration(); + sol.old.get_velocity() = sol.current.get_velocity(); + if (sim->com_mod.dFlag) + sol.old.get_displacement() = sol.current.get_displacement(); + } + + // Stop condition + int stopTS = nTS; + if (cm.mas(cm_mod)) { + if (FILE* fp = fopen(main_com.stopTrigName.c_str(), "r")) { + int count = fscanf(fp, "%d", &stopTS); + if (count == 0) stopTS = cTS; + fclose(fp); + } + } + cm.bcast(cm_mod, &stopTS); + if (cTS >= stopTS) break; + } +} + +//---------------------------------------------------------------------- +// compute_interface_velocity — Newmark-consistent velocity from disp_prev_. +// Each rank computes its local solid face nodes, then all-gather to +// produce the global vel_prev_ replicated on all ranks. +//---------------------------------------------------------------------- +void PartitionedFSI::compute_interface_velocity() +{ + auto& solid_com = solid_sim_->com_mod; + auto& solid_sol = solid_sim_->get_integrator().get_solutions(); + const auto& eq = solid_com.eq[0]; + const int s = eq.s; + const int nsd = main_sim_->com_mod.nsd; + const double dt = solid_com.dt; + const auto& Do = solid_sol.old.get_displacement(); + const auto& Yo = solid_sol.old.get_velocity(); + const auto& Ao = solid_sol.old.get_acceleration(); + auto& cm = main_sim_->com_mod.cm; + auto& cm_mod = main_sim_->cm_mod; + + // Compute velocity for this rank's local solid face nodes + Array local_vel(nsd, solid_face_->nNo); + for (int a = 0; a < solid_face_->nNo; a++) { + int Ac = solid_face_->gN(a); + for (int i = 0; i < nsd; i++) { + double disp_a = disp_prev_(i, solid_face_local_offset_ + a); + double a_new, v_new; + newmark::state_from_displacement( + disp_a, Do(i + s, Ac), Yo(i + s, Ac), Ao(i + s, Ac), + dt, eq.beta, eq.gam, a_new, v_new); + local_vel(i, a) = v_new; + } + } + + // All-gather to global + vel_prev_ = gather_face_data(local_vel, solid_face_global_nNo_, + solid_face_local_offset_, cm, cm_mod); +} + +//---------------------------------------------------------------------- +// solve_fluid — fluid equation with interface velocity and ALE. +// vel_prev_ is global; extract this rank's local fluid face portion. +//---------------------------------------------------------------------- +bool PartitionedFSI::solve_fluid( + const Array& mesh_vel_Yo, const Array& mesh_vel_Yn) +{ + auto& fluid_com = fluid_sim_->com_mod; + auto& fluid_int = fluid_sim_->get_integrator(); + auto& fluid_sol = fluid_int.get_solutions(); + const int nsd = main_sim_->com_mod.nsd; + + // Transfer global solid velocity → global fluid velocity, then extract local + auto global_fluid_vel = transfer_data(solid_to_fluid_map_, vel_prev_, + fluid_face_global_nNo_); + Array local_fluid_vel(nsd, fluid_face_->nNo); + for (int a = 0; a < fluid_face_->nNo; a++) + for (int i = 0; i < nsd; i++) + local_fluid_vel(i, a) = global_fluid_vel(i, fluid_face_local_offset_ + a); + + set_bc::set_bc_dir(fluid_com, fluid_sol); + fsi_coupling::apply_velocity_on_fluid( + fluid_com, fluid_com.eq[0], *fluid_face_, local_fluid_vel, fluid_sol); + + // ALE mesh velocity at generalized-alpha intermediate time + double af = fluid_com.eq[0].af; + fluid_com.ale_mesh_velocity.resize(nsd, fluid_com.tnNo); + for (int a = 0; a < fluid_com.tnNo; a++) + for (int i = 0; i < nsd; i++) + fluid_com.ale_mesh_velocity(i, a) = (1.0 - af) * mesh_vel_Yo(i, a) + + af * mesh_vel_Yn(i, a); + + fluid_int.step_equation(0, [&]() { + set_bc::enforce_dirichlet_dofs_on_face(fluid_com, *fluid_face_, 0, nsd); + }); + return !has_nan(fluid_sol); +} + +//---------------------------------------------------------------------- +// solve_solid — extract traction from fluid, solve solid. +// All-gathers local fluid traction to global, transfers to global solid, +// then extracts this rank's local solid portion. +//---------------------------------------------------------------------- +bool PartitionedFSI::solve_solid() +{ + auto& fluid_com = fluid_sim_->com_mod; + auto& solid_com = solid_sim_->com_mod; + auto& fluid_int = fluid_sim_->get_integrator(); + auto& solid_int = solid_sim_->get_integrator(); + auto& solid_sol = solid_int.get_solutions(); + auto& cm = main_sim_->com_mod.cm; + auto& cm_mod = main_sim_->cm_mod; + + // Compute local fluid traction, all-gather to global fluid face + auto local_fluid_traction = post::compute_face_traction( + fluid_com, fluid_sim_->cm_mod, + *fluid_mesh_, *fluid_face_, fluid_com.eq[0], + fluid_int.get_solutions()); + auto global_fluid_traction = gather_face_data(local_fluid_traction, + fluid_face_global_nNo_, + fluid_face_local_offset_, + cm, cm_mod); + + // Transfer global fluid → global solid, then extract local solid portion + auto global_solid_traction = transfer_data(fluid_to_solid_map_, + global_fluid_traction, + solid_face_global_nNo_); + const int nrows = global_solid_traction.nrows(); + Array local_solid_traction(nrows, solid_face_->nNo); + for (int a = 0; a < solid_face_->nNo; a++) + for (int i = 0; i < nrows; i++) + local_solid_traction(i, a) = + global_solid_traction(i, solid_face_local_offset_ + a); + + set_bc::set_bc_dir(solid_com, solid_sol); + solid_int.step_equation(0, [&]() { + fsi_coupling::apply_traction_on_solid( + solid_com, solid_com.eq[0], *solid_face_, local_solid_traction); + }); + return !has_nan(solid_sol); +} + +//---------------------------------------------------------------------- +// solve_mesh — mesh equation with relaxed displacement, deform fluid mesh. +// disp_prev_ is global; extract this rank's local mesh face portion. +//---------------------------------------------------------------------- +bool PartitionedFSI::solve_mesh(const Array& x_ref, int mesh_s) +{ + auto& fluid_com = fluid_sim_->com_mod; + auto& mesh_com = mesh_sim_->com_mod; + auto& mesh_int = mesh_sim_->get_integrator(); + auto& mesh_sol = mesh_int.get_solutions(); + const int nsd = main_sim_->com_mod.nsd; + + // Transfer global solid displacement → global mesh, extract local portion + auto global_mesh_disp = transfer_data(solid_to_mesh_map_, disp_prev_, + mesh_face_global_nNo_); + Array local_mesh_disp(nsd, mesh_face_->nNo); + for (int a = 0; a < mesh_face_->nNo; a++) + for (int i = 0; i < nsd; i++) + local_mesh_disp(i, a) = global_mesh_disp(i, mesh_face_local_offset_ + a); + + set_bc::set_bc_dir(mesh_com, mesh_sol); + fsi_coupling::apply_displacement_on_mesh( + mesh_com, mesh_com.eq[0], *mesh_face_, local_mesh_disp, mesh_sol); + mesh_int.step_equation(0, [&]() { + set_bc::enforce_dirichlet_on_face(mesh_com, *mesh_face_, nsd); + }); + if (has_nan(mesh_sol)) return false; + + // Deform fluid mesh: apply only the INCREMENT (Dn - Do) to x_ref + // so that fluid_com.x = x_original + Dn + auto& mesh_Dn = mesh_sol.current.get_displacement(); + auto& mesh_Do = mesh_sol.old.get_displacement(); + for (int a = 0; a < fluid_com.tnNo; a++) + for (int i = 0; i < nsd; i++) + fluid_com.x(i, a) = x_ref(i, a) + + mesh_Dn(i + mesh_s, a) - mesh_Do(i + mesh_s, a); + return true; +} + +//====================================================================== +// step — one coupling iteration loop for one time step +//====================================================================== +bool PartitionedFSI::step() +{ + auto& fluid_com = fluid_sim_->com_mod; + auto& solid_com = solid_sim_->com_mod; + auto& mesh_com = mesh_sim_->com_mod; + auto& cm_mod = main_sim_->cm_mod; + auto& cm = main_sim_->com_mod.cm; + const int nsd = main_sim_->com_mod.nsd; + const int cTS = main_sim_->com_mod.cTS; + + auto& fluid_sol = fluid_sim_->get_integrator().get_solutions(); + auto& solid_sol = solid_sim_->get_integrator().get_solutions(); + auto& mesh_sol = mesh_sim_->get_integrator().get_solutions(); + + omega_ = config_.initial_relaxation; + r_prev_.clear(); + + // Save predictor state + struct SavedState { Array An, Yn, Dn; }; + auto save_state = [](SolutionStates& s) -> SavedState { + return {s.current.get_acceleration(), s.current.get_velocity(), s.current.get_displacement()}; + }; + auto restore_state = [](SolutionStates& s, const SavedState& st) { + s.current.get_acceleration() = st.An; + s.current.get_velocity() = st.Yn; + s.current.get_displacement() = st.Dn; + }; + SavedState fluid_pred = save_state(fluid_sol); + SavedState solid_pred = save_state(solid_sol); + SavedState mesh_pred = save_state(mesh_sol); + + // Save mesh coordinates at start of time step = x_original + Do + Array x_ref(fluid_com.x); + + // ALE mesh velocity from predictor (updated after each mesh solve) + const int mesh_s = mesh_com.eq[0].s; + Array mesh_vel_Yn(nsd, mesh_com.tnNo); + Array mesh_vel_Yo(nsd, mesh_com.tnNo); + { + auto& mYn = mesh_sol.current.get_velocity(); + auto& mYo = mesh_sol.old.get_velocity(); + for (int a = 0; a < mesh_com.tnNo; a++) + for (int i = 0; i < nsd; i++) { + mesh_vel_Yn(i, a) = mYn(mesh_s + i, a); + mesh_vel_Yo(i, a) = mYo(mesh_s + i, a); + } + } + + // Initial interface state from predictor — extract local, all-gather to global + Array disp_current; + { + auto local_disp = fsi_coupling::extract_solid_displacement( + solid_com, solid_com.eq[0], *solid_face_, solid_sol); + disp_prev_ = gather_face_data(local_disp, solid_face_global_nNo_, + solid_face_local_offset_, cm, cm_mod); + } + compute_interface_velocity(); + + bool converged = false; + + for (int cp = 0; cp < config_.max_coupling_iterations; cp++) { + + // Restore all sub-sims to predictor state + restore_state(fluid_sol, fluid_pred); + restore_state(solid_sol, solid_pred); + restore_state(mesh_sol, mesh_pred); + + // ---- 1. Mesh solve + deform fluid mesh ---- + // Use latest disp_prev_ (relaxed from previous iter, or predictor on iter 0). + // Writes fluid_com.x = x_ref + (Dn - Do) so the fluid solves on the deformed mesh. + if (!solve_mesh(x_ref, mesh_s)) { + if (cm.mas(cm_mod)) std::cout << " ABORT: NaN in mesh solve" << std::endl; + return false; + } + + // Update ALE mesh velocity from this iteration's mesh solve + { + auto& mYn = mesh_sol.current.get_velocity(); + for (int a = 0; a < mesh_com.tnNo; a++) + for (int i = 0; i < nsd; i++) + mesh_vel_Yn(i, a) = mYn(mesh_s + i, a); + } + + // ---- 2. Fluid solve ---- + if (!solve_fluid(mesh_vel_Yo, mesh_vel_Yn)) { + if (cm.mas(cm_mod)) std::cout << " ABORT: NaN in fluid solve" << std::endl; + return false; + } + + // ---- 3. Solid solve ---- + if (!solve_solid()) { + if (cm.mas(cm_mod)) std::cout << " ABORT: NaN in solid solve" << std::endl; + return false; + } + + // ---- 4. Extract displacement (global), check convergence ---- + // Extract local solid displacement and all-gather to global so all ranks + // have identical arrays — no MPI reduction needed for norms. + { + auto local_disp = fsi_coupling::extract_solid_displacement( + solid_com, solid_com.eq[0], *solid_face_, solid_sol); + disp_current = gather_face_data(local_disp, solid_face_global_nNo_, + solid_face_local_offset_, cm, cm_mod); + } + + double res_norm = 0.0, disp_norm = 0.0; + for (int a = 0; a < solid_face_global_nNo_; a++) + for (int i = 0; i < nsd; i++) { + double res = disp_current(i, a) - disp_prev_(i, a); + res_norm += res * res; + disp_norm += disp_current(i, a) * disp_current(i, a); + } + res_norm = sqrt(res_norm); + disp_norm = sqrt(disp_norm); + double rel = (disp_norm > 1e-30) ? res_norm / disp_norm : res_norm; + + // ---- 5. Relaxation ---- + relax_interface(cp, nsd, disp_current); + compute_interface_velocity(); + + // Check for NaN/divergence (global arrays — consistent on all ranks) + { + bool bad = false; + double max_disp = 0; + for (int a = 0; a < solid_face_global_nNo_ && !bad; a++) + for (int i = 0; i < nsd; i++) { + if (std::isnan(disp_prev_(i, a)) || std::isinf(disp_prev_(i, a))) + { bad = true; break; } + max_disp = std::max(max_disp, std::abs(disp_prev_(i, a))); + } + if (bad || max_disp > 1e10) { + if (cm.mas(cm_mod)) std::cout << " ABORT: NaN/divergence after relaxation" << std::endl; + return false; + } + } + + // ---- 6. Output ---- + if (cp == 0) first_res_norm_ = res_norm; + int dB_val = 0; + double ri_r1 = 1.0; + if (first_res_norm_ > 1e-30 && res_norm > 0) { + ri_r1 = res_norm / first_res_norm_; + dB_val = static_cast(20.0 * log10(ri_r1)); + } + + if (cm.mas(cm_mod)) { + bool conv = rel < config_.coupling_tolerance; + bool saved = conv + && (cTS % fluid_sim_->com_mod.saveIncr == 0) + && (cTS >= fluid_sim_->com_mod.saveATS); + char buf[256]; + snprintf(buf, sizeof(buf), " CP %d-%d%s %10.3e %5d %10.3e %10.3e %10.3e %10.3e", + cTS, cp + 1, saved ? "s" : " ", + main_sim_->com_mod.timer.get_elapsed_time(), + dB_val, ri_r1, rel, omega_, disp_norm); + std::cout << buf << std::endl; + if (coupling_log_.is_open()) coupling_log_ << buf << std::endl; + if (histor_log_.is_open()) histor_log_ << buf << std::endl; + } + + if (rel < config_.coupling_tolerance) { converged = true; break; } + } + return converged; +} + +//---------------------------------------------------------------------- +// save_results +//---------------------------------------------------------------------- +void PartitionedFSI::save_results() +{ + int cTS = main_sim_->com_mod.cTS; + Simulation* sims[3] = {fluid_sim_.get(), solid_sim_.get(), mesh_sim_.get()}; + + for (auto* sim : sims) { + auto& com = sim->com_mod; + auto& sol = sim->get_integrator().get_solutions(); + if (com.saveVTK) { + bool l2 = ((cTS % com.saveIncr) == 0); + bool l3 = (cTS >= com.saveATS); + if (l2 && l3) { + output::output_result(sim, com.timeP, 3, 0); + vtk_xml::write_vtus(sim, sol, false); + } + } + } +} + diff --git a/Code/Source/solver/PartitionedFSI.h b/Code/Source/solver/PartitionedFSI.h new file mode 100644 index 000000000..eaabb7dd2 --- /dev/null +++ b/Code/Source/solver/PartitionedFSI.h @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef PARTITIONED_FSI_H +#define PARTITIONED_FSI_H + +#include "Simulation.h" +#include "Integrator.h" +#include "Array.h" +#include "CmMod.h" + +#include +#include +#include +#include + +/// @brief Coupling method for interface relaxation +enum class CouplingMethod { constant, aitken }; + +/// @brief Configuration for partitioned FSI coupling, read from XML input. +struct PartitionedFSIConfig { + int max_coupling_iterations = 50; + double coupling_tolerance = 1e-6; + double initial_relaxation = 1.0; + double omega_max = 1.0; + CouplingMethod coupling_method = CouplingMethod::aitken; + + // Face names for the FSI interface + std::string fluid_interface_face; + std::string solid_interface_face; +}; + +/// @brief Partitioned FSI coupling with 3 independent sub-Simulations. +/// +/// Each sub-field (fluid, struct, mesh) has its own Simulation object with +/// independent mesh, solution arrays, and linear system. No shared global +/// arrays, no DOF offsets (each eq.s=0), no regularization of inactive nodes. +/// +/// Implements Dirichlet-Neumann coupling with Aitken relaxation: +/// 1. Transfer solid displacement to mesh interface, solve mesh equation +/// 2. Deform fluid mesh using mesh displacement, solve fluid equation +/// 3. Extract fluid traction, apply to solid, solve solid equation +/// 4. Extract solid displacement, apply Aitken relaxation +/// 5. Check coupling convergence +/// +/// Related to GitHub issue #431: Implement partitioned FSI in svMultiPhysics +class PartitionedFSI { +public: + PartitionedFSI(Simulation* main_simulation, const PartitionedFSIConfig& config, + const std::string& xml_file_path); + + ~PartitionedFSI(); + + void run(); + bool step(); + +private: + Simulation* main_sim_; + PartitionedFSIConfig config_; + std::string xml_file_path_; + + // Sub-simulations (owned) + std::unique_ptr fluid_sim_; + std::unique_ptr solid_sim_; + std::unique_ptr mesh_sim_; + + // Interface face pointers within each sub-sim + const faceType* fluid_face_ = nullptr; + const faceType* solid_face_ = nullptr; + const faceType* mesh_face_ = nullptr; + + // Mesh pointers within each sub-sim + const mshType* fluid_mesh_ = nullptr; + const mshType* solid_mesh_ = nullptr; + const mshType* mesh_mesh_ = nullptr; + + // Global face node counts and per-rank offsets (for MPI distribution) + int solid_face_global_nNo_ = 0; + int solid_face_local_offset_ = 0; + int fluid_face_global_nNo_ = 0; + int fluid_face_local_offset_ = 0; + int mesh_face_global_nNo_ = 0; + int mesh_face_local_offset_ = 0; + + // Node maps between interface faces: global_src_idx → global_tgt_idx + std::vector solid_to_fluid_map_; + std::vector fluid_to_solid_map_; + std::vector solid_to_mesh_map_; + + // Coupling state — indexed by GLOBAL solid face nodes (replicated on all ranks) + Array disp_prev_; + Array vel_prev_; + double omega_; + double first_res_norm_ = 0.0; + + // Aitken state — sized for global solid face + std::vector r_prev_; + + // Output files for coupling convergence history + std::ofstream coupling_log_; + std::ofstream histor_log_; + + // Temp XML file paths (cleaned up in destructor) + std::vector temp_xml_paths_; + + void resolve_faces(); + void build_node_maps(); + + /// Solve fluid equation with current interface velocity and ALE mesh velocity + bool solve_fluid(const Array& mesh_vel_Yo, const Array& mesh_vel_Yn); + + /// Extract fluid traction, transfer to solid, solve solid equation + bool solve_solid(); + + /// Solve mesh equation with relaxed displacement, deform fluid mesh + bool solve_mesh(const Array& x_ref, int mesh_s); + + /// Compute vel_prev_ (global) from disp_prev_ (global) using Newmark relationship + void compute_interface_velocity(); + + void relax_interface(int cp, int nsd, const Array& disp_current); + void relax_constant(int cp, int nsd, const Array& disp_current); + void relax_aitken(int cp, int nsd, const Array& disp_current); + + /// Compute global_nNo and local_offset for a distributed face + static void compute_face_global_info(const faceType& face, cmType& cm, + const CmMod& cm_mod, + int& global_nNo, int& local_offset); + + /// All-gather local face data (nrows, local_nNo) to global (nrows, global_nNo) + static Array gather_face_data(const Array& local_data, + int global_nNo, int local_offset, + cmType& cm, const CmMod& cm_mod); + + /// All-gather local (local_src → global_tgt) map to global (global_src → global_tgt) map + static void gather_global_map(const std::vector& local_map, + int global_src_nNo, + cmType& cm, const CmMod& cm_mod, + std::vector& global_map); + + /// Build local face_a → global face_b node map using pre-gathered global face_b coords + static void build_face_node_map(const faceType& face_a, const ComMod& com_a, + int global_b_nNo, const Array& global_b_coords, + std::vector& a_to_global_b); + + /// Build a minimal sub-simulation XML for the given role by extracting + /// the tagged equation and its meshes from the main XML. Returns temp file path. + static std::string build_sub_xml(const std::string& main_xml_path, + const std::string& role); + + /// Transfer data from global src face to global tgt face using global map + static Array transfer_data(const std::vector& src_to_tgt_map, + const Array& src_data, int tgt_nNo); + + void save_results(); +}; + +#endif // PARTITIONED_FSI_H diff --git a/Code/Source/solver/Simulation.cpp b/Code/Source/solver/Simulation.cpp index 7e6d56523..a98f769bb 100644 --- a/Code/Source/solver/Simulation.cpp +++ b/Code/Source/solver/Simulation.cpp @@ -3,6 +3,7 @@ #include "Simulation.h" #include "Integrator.h" +#include "PartitionedFSI.h" #include "all_fun.h" #include "load_msh.h" @@ -12,6 +13,17 @@ #include #include +void add_eq_linear_algebra(ComMod& com_mod, eqType& lEq) +{ + lEq.linear_algebra = LinearAlgebraFactory::create_interface(lEq.linear_algebra_type); + lEq.linear_algebra->set_preconditioner(lEq.linear_algebra_preconditioner); + lEq.linear_algebra->initialize(com_mod, lEq); + + if (lEq.linear_algebra_assembly_type != consts::LinearAlgebraType::none) { + lEq.linear_algebra->set_assembly(lEq.linear_algebra_assembly_type); + } +} + Simulation::Simulation() { roInf = 0.2; @@ -112,3 +124,81 @@ Integrator& Simulation::get_integrator() } return *integrator_; } + +/// @brief Get pointer to PartitionedFSI object (null if not configured) +PartitionedFSI* Simulation::get_partitioned_fsi() +{ + return partitioned_fsi_.get(); +} + +/// @brief Initialize partitioned FSI if configured in parameters. +/// +/// Parameters are only parsed on rank 0 (slaves skip read_files), so we +/// broadcast the active flag and config to all ranks before branching. +void Simulation::initialize_partitioned_fsi(const std::string& xml_file_path) +{ + auto& cm = com_mod.cm; + auto& cm_mod_ref = cm_mod; + + // Rank 0 determines whether partitioned FSI is active and builds the config. + // Broadcast the decision so all ranks take the same path. + int active = 0; + PartitionedFSIConfig config; + + if (cm.mas(cm_mod_ref)) { + auto& pcp = parameters.partitioned_coupling_parameters; + // Active when is present and at least one equation + // carries a partitioned role attribute. + if (pcp.defined()) { + for (auto* ep : parameters.equation_parameters) { + if (ep->role.defined() && !ep->role.value().empty()) { active = 1; break; } + } + } + if (active) { + config.max_coupling_iterations = pcp.max_coupling_iterations.value(); + config.coupling_tolerance = pcp.coupling_tolerance.value(); + config.initial_relaxation = pcp.initial_relaxation.value(); + config.omega_max = pcp.omega_max.value(); + + std::string method = pcp.coupling_method.value(); + if (method == "constant") config.coupling_method = CouplingMethod::constant; + else if (method == "aitken") config.coupling_method = CouplingMethod::aitken; + else throw std::runtime_error("[PartitionedFSI] Unknown Coupling_method: " + method); + + config.fluid_interface_face = pcp.fluid_interface_face.value(); + config.solid_interface_face = pcp.solid_interface_face.value(); + } + } + + // Broadcast the active flag and config fields to all ranks. + MPI_Bcast(&active, 1, MPI_INT, 0, cm.com()); + if (!active) return; + + int max_iter = config.max_coupling_iterations; + double tol = config.coupling_tolerance; + double relax = config.initial_relaxation; + double omax = config.omega_max; + int method_i = static_cast(config.coupling_method); + MPI_Bcast(&max_iter, 1, MPI_INT, 0, cm.com()); + MPI_Bcast(&tol, 1, MPI_DOUBLE, 0, cm.com()); + MPI_Bcast(&relax, 1, MPI_DOUBLE, 0, cm.com()); + MPI_Bcast(&omax, 1, MPI_DOUBLE, 0, cm.com()); + MPI_Bcast(&method_i, 1, MPI_INT, 0, cm.com()); + + auto bcast_str = [&](std::string& s) { + int len = static_cast(s.size()); + MPI_Bcast(&len, 1, MPI_INT, 0, cm.com()); + s.resize(len); + MPI_Bcast(s.data(), len, MPI_CHAR, 0, cm.com()); + }; + bcast_str(config.fluid_interface_face); + bcast_str(config.solid_interface_face); + + config.max_coupling_iterations = max_iter; + config.coupling_tolerance = tol; + config.initial_relaxation = relax; + config.omega_max = omax; + config.coupling_method = static_cast(method_i); + + partitioned_fsi_ = std::make_unique(this, config, xml_file_path); +} diff --git a/Code/Source/solver/Simulation.h b/Code/Source/solver/Simulation.h index a8e092158..afd2081c4 100644 --- a/Code/Source/solver/Simulation.h +++ b/Code/Source/solver/Simulation.h @@ -13,8 +13,11 @@ #include #include -// Forward declaration +// Forward declarations class Integrator; +class PartitionedFSI; + +void add_eq_linear_algebra(ComMod& com_mod, eqType& lEq); class Simulation { @@ -28,6 +31,8 @@ class Simulation { ChnlMod& get_chnl_mod() { return chnl_mod; }; ComMod& get_com_mod() { return com_mod; }; Integrator& get_integrator(); + PartitionedFSI* get_partitioned_fsi(); + void initialize_partitioned_fsi(const std::string& xml_file_path); // Initialize the Integrator object after simulation setup is complete // Takes ownership of solution states via move semantics @@ -75,6 +80,9 @@ class Simulation { private: // Time integrator for Newton iteration loop std::unique_ptr integrator_; + + // Partitioned FSI coupling (null if not configured) + std::unique_ptr partitioned_fsi_; }; #endif diff --git a/Code/Source/solver/SimulationLogger.h b/Code/Source/solver/SimulationLogger.h index 4023079bb..03dd96b44 100755 --- a/Code/Source/solver/SimulationLogger.h +++ b/Code/Source/solver/SimulationLogger.h @@ -33,6 +33,8 @@ class SimulationLogger { bool is_initialized() const { return log_file_.is_open(); } + void set_cout_write(bool v) const { cout_write_ = v; } + ~SimulationLogger() { log_file_.close(); diff --git a/Code/Source/solver/eq_assem.cpp b/Code/Source/solver/eq_assem.cpp index 7ec8c1c1f..6fa8252ff 100644 --- a/Code/Source/solver/eq_assem.cpp +++ b/Code/Source/solver/eq_assem.cpp @@ -58,9 +58,13 @@ void b_assem_neu_bc(ComMod& com_mod, const faceType& lFa, const Vector& cDmn = all_fun::domain(com_mod, msh, cEq, Ec); auto cPhys = eq.dmn[cDmn].phys; - Vector ptr(eNoN); - Vector N(eNoN), hl(eNoN); - Array yl(tDof,eNoN), lR(dof,eNoN); + // For ALE partitioned FSI, extend yl to hold mesh velocity + const bool has_ale = (com_mod.ale_mesh_velocity.size() > 0); + const int yl_nrows = has_ale ? tDof + nsd : tDof; + + Vector ptr(eNoN); + Vector N(eNoN), hl(eNoN); + Array yl(yl_nrows,eNoN), lR(dof,eNoN); Array3 lK(dof*dof,eNoN,eNoN); for (int a = 0; a < eNoN; a++) { @@ -70,6 +74,11 @@ void b_assem_neu_bc(ComMod& com_mod, const faceType& lFa, const Vector& for (int i = 0; i < tDof; i++) { yl(i,a) = Yg(i,Ac); } + if (has_ale) { + for (int i = 0; i < nsd; i++) { + yl(nsd+1+i, a) = com_mod.ale_mesh_velocity(i, Ac); + } + } } // Updating the shape functions, if neccessary @@ -87,7 +96,7 @@ void b_assem_neu_bc(ComMod& com_mod, const faceType& lFa, const Vector& N = lFa.N.col(g); double h = 0.0; - Vector y(tDof); + Vector y(yl_nrows); for (int a = 0; a < eNoN; a++) { h = h + N(a)*hl(a); @@ -162,6 +171,9 @@ void b_assem_neu_bc(ComMod& com_mod, const faceType& lFa, const Vector& /// @param Dg void b_neu_folw_p(ComMod& com_mod, const bcType& lBc, const faceType& lFa, const Vector& hg, const SolutionStates& solutions) { + // Local alias for old displacement + const auto& Do = solutions.old.get_displacement(); + using namespace consts; using namespace utils; const auto& Dg = solutions.intermediate.get_displacement(); @@ -293,6 +305,10 @@ void b_neu_folw_p(ComMod& com_mod, const bcType& lBc, const faceType& lFa, const /// an arbitrary vector. void fsi_ls_upd(ComMod& com_mod, const bcType& lBc, const faceType& lFa, const SolutionStates& solutions) { + // Local aliases for displacement arrays + const auto& Dn = solutions.current.get_displacement(); + const auto& Do = solutions.old.get_displacement(); + using namespace consts; using namespace utils; using namespace fsi_linear_solver; diff --git a/Code/Source/solver/fluid.cpp b/Code/Source/solver/fluid.cpp index 2bf56dfb0..e15820d53 100644 --- a/Code/Source/solver/fluid.cpp +++ b/Code/Source/solver/fluid.cpp @@ -50,7 +50,7 @@ void b_fluid(ComMod& com_mod, const int eNoN, const double w, const Vector u(nsd); - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int i = 0; i < nsd; i++) { int j = i + nsd + 1; u(i) = y(i) - y(j); @@ -162,7 +162,7 @@ void bw_fluid_2d(ComMod& com_mod, const int eNoNw, const int eNoNq, const double Vector uh(2); - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { uh(0) = uh(0) + Nw(a)*yl(3,a); uh(1) = uh(1) + Nw(a)*yl(4,a); @@ -318,7 +318,7 @@ void bw_fluid_3d(ComMod& com_mod, const int eNoNw, const int eNoNq, const double Vector uh(3); - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { uh(0) = uh(0) + Nw(a)*yl(4,a); uh(1) = uh(1) + Nw(a)*yl(5,a); @@ -521,14 +521,18 @@ void construct_fluid(ComMod& com_mod, const mshType& lM, const SolutionStates& s #endif // FLUID: dof = nsd+1 - Vector ptr(eNoN); - Array xl(nsd,eNoN); - + // For ALE partitioned FSI, extend yl to hold mesh velocity at DOFs nsd+1..2*nsd + const bool has_ale = (com_mod.ale_mesh_velocity.size() > 0); + const int yl_nrows = has_ale ? tDof + nsd : tDof; + + Vector ptr(eNoN); + Array xl(nsd,eNoN); + // local acceleration vector (for a single element) Array al(tDof,eNoN); - + // local velocity vector (for a single element) - Array yl(tDof,eNoN); + Array yl(yl_nrows,eNoN); Array bfl(nsd,eNoN); // local (weak form) residual vector (for a single element) @@ -570,10 +574,16 @@ void construct_fluid(ComMod& com_mod, const mshType& lM, const SolutionStates& s xl(i,a) = com_mod.x(i,Ac); bfl(i,a) = com_mod.Bf(i,Ac); } - for (int i = 0; i < al.nrows(); i++) { + for (int i = 0; i < tDof; i++) { al(i,a) = Ag(i,Ac); yl(i,a) = Yg(i,Ac); } + // Append ALE mesh velocity at DOFs nsd+1..2*nsd (same layout as mvMsh) + if (has_ale) { + for (int i = 0; i < nsd; i++) { + yl(nsd+1+i, a) = com_mod.ale_mesh_velocity(i, Ac); + } + } } // Initialize residual and tangents @@ -895,7 +905,7 @@ void fluid_2d_c(ComMod& com_mod, const int vmsFlag, const int eNoNw, const int e } // Update convection velocity relative to mesh velocity - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { u(0) = u(0) - Nw(a)*yl(3,a); u(1) = u(1) - Nw(a)*yl(4,a); @@ -1212,7 +1222,7 @@ void fluid_2d_m(ComMod& com_mod, const int vmsFlag, const int eNoNw, const int e } // Update convection velocity relative to mesh velocity - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { u(0) = u(0) - Nw(a)*yl(3,a); u(1) = u(1) - Nw(a)*yl(4,a); @@ -1569,7 +1579,7 @@ void fluid_3d_c(ComMod& com_mod, const int vmsFlag, const int eNoNw, const int e // Update convection velocity relative to mesh velocity // - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { u[0] = u[0] - Nw(a)*yl(4,a); u[1] = u[1] - Nw(a)*yl(5,a); @@ -1920,7 +1930,7 @@ void fluid_3d_m(ComMod& com_mod, const int vmsFlag, const int eNoNw, const int e // Update convection velocity relative to mesh velocity // - if (com_mod.mvMsh) { + if (com_mod.mvMsh || com_mod.ale_mesh_velocity.size() > 0) { for (int a = 0; a < eNoNw; a++) { u[0] = u[0] - Nw(a)*yl(4,a); u[1] = u[1] - Nw(a)*yl(5,a); diff --git a/Code/Source/solver/fsi_coupling.cpp b/Code/Source/solver/fsi_coupling.cpp new file mode 100644 index 000000000..540669e48 --- /dev/null +++ b/Code/Source/solver/fsi_coupling.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "fsi_coupling.h" +#include "Integrator.h" + +namespace fsi_coupling { + +//---------------------------------------------------------------------- +// extract_solid_displacement +//---------------------------------------------------------------------- +Array extract_solid_displacement( + const ComMod& com_mod, const eqType& solid_eq, + const faceType& lFa, const SolutionStates& solutions) +{ + const int nsd = com_mod.nsd; + const int s = solid_eq.s; // DOF offset for the solid equation + const auto& Dn = solutions.current.get_displacement(); + + Array result(nsd, lFa.nNo); + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < nsd; i++) { + result(i, a) = Dn(i + s, Ac); + } + } + return result; +} + +//---------------------------------------------------------------------- +// apply_velocity_on_fluid +//---------------------------------------------------------------------- +void apply_velocity_on_fluid( + ComMod& com_mod, const eqType& fluid_eq, + const faceType& lFa, + const Array& velocity, + SolutionStates& solutions) +{ + const int nsd = com_mod.nsd; + const int s = fluid_eq.s; + const double dt = com_mod.dt; + const double gam = fluid_eq.gam; + + auto& An = solutions.current.get_acceleration(); + auto& Yn = solutions.current.get_velocity(); + const auto& Yo = solutions.old.get_velocity(); + const auto& Ao = solutions.old.get_acceleration(); + + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < nsd; i++) { + Yn(i + s, Ac) = velocity(i, a); + double a_new; + newmark::state_from_velocity( + velocity(i, a), Yo(i + s, Ac), Ao(i + s, Ac), dt, gam, a_new); + An(i + s, Ac) = a_new; + } + } +} + +//---------------------------------------------------------------------- +// apply_traction_on_solid +//---------------------------------------------------------------------- +void apply_traction_on_solid( + ComMod& com_mod, const eqType& solid_eq, + const faceType& lFa, + const Array& traction) +{ + // The traction array contains consistent nodal forces (external force on solid). + // In svMultiPhysics, external forces are SUBTRACTED from R (see b_l_elas: + // lR -= w*N*h). So R -= traction. + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < traction.nrows(); i++) { + com_mod.R(i, Ac) -= traction(i, a); + } + } +} + +//---------------------------------------------------------------------- +// apply_displacement_on_mesh +//---------------------------------------------------------------------- +void apply_displacement_on_mesh( + ComMod& com_mod, const eqType& mesh_eq, + const faceType& lFa, + const Array& displacement, + SolutionStates& solutions) +{ + const int nsd = com_mod.nsd; + const int s = mesh_eq.s; + const double dt = com_mod.dt; + const double gam = mesh_eq.gam; + const double beta = mesh_eq.beta; + + auto& An = solutions.current.get_acceleration(); + auto& Yn = solutions.current.get_velocity(); + auto& Dn = solutions.current.get_displacement(); + const auto& Do = solutions.old.get_displacement(); + const auto& Yo = solutions.old.get_velocity(); + const auto& Ao = solutions.old.get_acceleration(); + + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < nsd; i++) { + Dn(i + s, Ac) = displacement(i, a); + double a_new, v_new; + newmark::state_from_displacement( + displacement(i, a), Do(i + s, Ac), Yo(i + s, Ac), Ao(i + s, Ac), + dt, beta, gam, a_new, v_new); + An(i + s, Ac) = a_new; + Yn(i + s, Ac) = v_new; + } + } +} + +} // namespace fsi_coupling diff --git a/Code/Source/solver/fsi_coupling.h b/Code/Source/solver/fsi_coupling.h new file mode 100644 index 000000000..c32c5e569 --- /dev/null +++ b/Code/Source/solver/fsi_coupling.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef FSI_COUPLING_H +#define FSI_COUPLING_H + +#include "ComMod.h" +#include "SolutionStates.h" + +/// @brief FSI interface data exchange functions for partitioned coupling. +/// +/// These functions extract and apply fluid traction and solid displacement +/// at the FSI interface, enabling partitioned (Dirichlet-Neumann) coupling +/// between separately solved fluid and solid equations. +/// +/// Related to GitHub issue #431: Implement partitioned FSI in svMultiPhysics + +namespace fsi_coupling { + +/// @brief Extract solid displacement at interface face nodes. +/// +/// @param com_mod Common module +/// @param solid_eq The solid equation (for DOF offset) +/// @param solid_face The solid-side FSI interface face +/// @param solutions Solution states +/// @return Array(nsd, solid_face.nNo) of displacement values +Array extract_solid_displacement( + const ComMod& com_mod, const eqType& solid_eq, + const faceType& solid_face, const SolutionStates& solutions); + +/// @brief Apply velocity as strong Dirichlet BC on fluid interface nodes. +/// Directly sets Yn at the fluid equation DOF range for the face nodes. +void apply_velocity_on_fluid( + ComMod& com_mod, const eqType& fluid_eq, + const faceType& fluid_face, + const Array& velocity, + SolutionStates& solutions); + +/// @brief Apply pre-computed consistent nodal forces to the solid residual. +/// +/// Adds the traction forces directly to com_mod.R at the global node locations +/// corresponding to the solid face. This should be called during the +/// post-assembly callback of step_equation() for the solid equation. +/// +/// @param com_mod Common module (R is modified) +/// @param solid_eq The solid equation (for DOF offset) +/// @param solid_face The solid-side FSI interface face +/// @param traction Array(nsd, solid_face.nNo) of consistent nodal forces +void apply_traction_on_solid( + ComMod& com_mod, const eqType& solid_eq, + const faceType& solid_face, + const Array& traction); + +/// @brief Apply displacement as strong Dirichlet BC on mesh interface nodes. +/// +/// Directly sets the displacement in the solution arrays (An, Yn, Dn) for +/// the mesh equation DOF range at the interface face nodes. +/// +/// @param com_mod Common module +/// @param mesh_eq The mesh equation (for DOF offset) +/// @param mesh_face The mesh-side FSI interface face +/// @param displacement Array(nsd, mesh_face.nNo) of displacement values +/// @param solutions Solution states (modified) +void apply_displacement_on_mesh( + ComMod& com_mod, const eqType& mesh_eq, + const faceType& mesh_face, + const Array& displacement, + SolutionStates& solutions); + +} // namespace fsi_coupling + +#endif // FSI_COUPLING_H diff --git a/Code/Source/solver/main.cpp b/Code/Source/solver/main.cpp index 4e778f03e..377adec6a 100644 --- a/Code/Source/solver/main.cpp +++ b/Code/Source/solver/main.cpp @@ -9,6 +9,7 @@ // #include "Simulation.h" #include "Integrator.h" +#include "PartitionedFSI.h" #include "all_fun.h" #include "bf.h" @@ -36,21 +37,6 @@ #include #include -//------------------------ -// add_eq_linear_algebra -//------------------------ -// Create a LinearAlgebra object for an equation. -// -void add_eq_linear_algebra(ComMod& com_mod, eqType& lEq) -{ - lEq.linear_algebra = LinearAlgebraFactory::create_interface(lEq.linear_algebra_type); - lEq.linear_algebra->set_preconditioner(lEq.linear_algebra_preconditioner); - lEq.linear_algebra->initialize(com_mod, lEq); - - if (lEq.linear_algebra_assembly_type != consts::LinearAlgebraType::none) { - lEq.linear_algebra->set_assembly(lEq.linear_algebra_assembly_type); - } -} void finalize_linear_algebra(eqType& lEq) { @@ -350,6 +336,8 @@ void iterate_solution(Simulation* simulation) #endif int iEqOld = cEq; + + // Monolithic Newton iteration loop integrator.step(); #ifdef debug_iterate_solution @@ -569,7 +557,12 @@ void iterate_solution(Simulation* simulation) void run_simulation(Simulation* simulation) { - iterate_solution(simulation); + auto* partitioned_fsi = simulation->get_partitioned_fsi(); + if (partitioned_fsi) { + partitioned_fsi->run(); + } else { + iterate_solution(simulation); + } } @@ -655,6 +648,9 @@ int main(int argc, char *argv[]) add_eq_linear_algebra(simulation->com_mod, eq); } + // Initialize partitioned FSI coupling if configured + simulation->initialize_partitioned_fsi(file_name); + #ifdef debug_main for (int iM = 0; iM < simulation->com_mod.nMsh; iM++) { dmsg << "---------- iM " << iM; diff --git a/Code/Source/solver/mesh.cpp b/Code/Source/solver/mesh.cpp index b7b8cdfb9..c3d6c38cf 100644 --- a/Code/Source/solver/mesh.cpp +++ b/Code/Source/solver/mesh.cpp @@ -45,9 +45,12 @@ void construct_mesh(ComMod& com_mod, CepMod& cep_mod, const mshType& lM, const S auto& pSa = com_mod.pSa; bool pstEq = com_mod.pstEq; - // Start and end DOF - int is = nsd + 1; - int ie = 2*nsd; + // Start and end DOF for mesh equation in the global solution arrays. + // Use eq.s (DOF offset) instead of hardcoded nsd+1, so this works both + // in monolithic FSI (where mesh is the 3rd equation) and in partitioned + // FSI (where mesh is the only equation with eq.s=0). + int is = eq.s; + int ie = eq.s + nsd - 1; int eNoN = lM.eNoN; #ifdef debug_construct_mesh dmsg << "cEq: " << cEq; diff --git a/Code/Source/solver/post.cpp b/Code/Source/solver/post.cpp index 57bbe6194..0730a17ee 100644 --- a/Code/Source/solver/post.cpp +++ b/Code/Source/solver/post.cpp @@ -4,6 +4,7 @@ #include "post.h" #include "all_fun.h" +#include #include "fluid.h" #include "fs.h" #include "initialize.h" @@ -2124,4 +2125,148 @@ void tpost(Simulation* simulation, const mshType& lM, const int m, Array } } +//---------------------------------------------------------------------- +// compute_face_traction — consistent nodal forces at a fluid face +//---------------------------------------------------------------------- +// Computes f(i,a) = integral(sigma_ij * n_j * N_a dGamma) at each face +// node. Returns the force that the fluid exerts ON the solid. +// Adapts the stress computation from bpost() but integrates over the +// face using face Gauss quadrature. +// +Array compute_face_traction( + ComMod& com_mod, const CmMod& cm_mod, + const mshType& lM, const faceType& lFa, + const eqType& eq, + const SolutionStates& solutions) +{ + const auto& Yg = solutions.intermediate.get_velocity(); + const auto& Dg = solutions.intermediate.get_displacement(); + + const int nsd = com_mod.nsd; + const int eNoN = lM.eNoN; + + // Pressure function space (P1-P1 or Taylor-Hood P2-P1) + fsType fsP; + if (lM.nFs == 1) { + fsP.eNoN = lM.fs[0].eNoN; + fsP.N = lM.fs[0].N; + } else { + fsP.eNoN = lM.fs[1].eNoN; + fsP.nG = lM.fs[0].nG; + fsP.eType = lM.fs[1].eType; + fs::alloc_fs(fsP, nsd, nsd); + fsP.xi = lM.fs[0].xi; + for (int g = 0; g < fsP.nG; g++) { + nn::get_gnn(nsd, fsP.eType, fsP.eNoN, g, fsP.xi, fsP.N, fsP.Nx); + } + } + + Array result(nsd, lFa.nNo); + + std::unordered_map global_to_face; + global_to_face.reserve(lFa.nNo); + for (int a = 0; a < lFa.nNo; a++) { + global_to_face[lFa.gN(a)] = a; + } + + Array xl(nsd, eNoN); + Array ul(nsd, eNoN); + Vector pl(fsP.eNoN); + Array Nx(nsd, eNoN); + Array ks(nsd, nsd); + + for (int e = 0; e < lFa.nEl; e++) { + int Ec = lFa.gE(e); + int cEq = com_mod.cEq; + int cDmn = all_fun::domain(com_mod, lM, cEq, Ec); + if (cDmn == -1) continue; + + for (int a = 0; a < eNoN; a++) { + int Ac = lM.IEN(a, Ec); + for (int i = 0; i < nsd; i++) { + xl(i, a) = com_mod.x(i, Ac); + ul(i, a) = Yg(i, Ac); + } + } + for (int a = 0; a < fsP.eNoN; a++) { + int Ac = lM.IEN(a, Ec); + pl(a) = Yg(nsd, Ac); + } + + Array sigma_avg(nsd, nsd); + double Jac_vol; + + for (int g = 0; g < lM.nG; g++) { + if (g == 0 || !lM.lShpF) { + auto lM_Nx = lM.Nx.slice(g); + nn::gnn(eNoN, nsd, nsd, lM_Nx, xl, Nx, Jac_vol, ks); + } + + Array ux(nsd, nsd); + for (int a = 0; a < eNoN; a++) + for (int i = 0; i < nsd; i++) + for (int j = 0; j < nsd; j++) + ux(i, j) += Nx(i, a) * ul(j, a); + + double p = 0.0; + for (int a = 0; a < fsP.eNoN; a++) + p += fsP.N(a, g) * pl(a); + + double gam = 0.0; + for (int i = 0; i < nsd; i++) + for (int j = 0; j < nsd; j++) + gam += (ux(i, j) + ux(j, i)) * (ux(i, j) + ux(j, i)); + gam = sqrt(0.5 * gam); + + double mu, mu_s; + fluid::get_viscosity(com_mod, eq.dmn[cDmn], gam, mu, mu_s, mu_s); + + for (int i = 0; i < nsd; i++) + for (int j = 0; j < nsd; j++) + sigma_avg(i, j) += (-p * (i == j ? 1.0 : 0.0) + + mu * (ux(i, j) + ux(j, i))) + / static_cast(lM.nG); + } + + for (int gf = 0; gf < lFa.nG; gf++) { + Vector nV(nsd); + auto face_Nx = lFa.Nx.slice(gf); + nn::gnnb(com_mod, lFa, e, gf, nsd, nsd - 1, lFa.eNoN, face_Nx, nV, + solutions, consts::MechanicalConfigurationType::reference); + double Jac_face = sqrt(utils::norm(nV)); + double w = lFa.w(gf) * Jac_face; + for (int i = 0; i < nsd; i++) nV(i) /= Jac_face; + + Vector trac(nsd); + for (int i = 0; i < nsd; i++) + for (int j = 0; j < nsd; j++) + trac(i) += sigma_avg(i, j) * nV(j); + + auto N = lFa.N.col(gf); + for (int a = 0; a < lFa.eNoN; a++) { + int Ac = lFa.IEN(a, e); + int a_local = global_to_face[Ac]; + for (int i = 0; i < nsd; i++) + result(i, a_local) -= w * N(a) * trac(i); + } + } + } + + // MPI communication + Array gResult(nsd, com_mod.tnNo); + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < nsd; i++) + gResult(i, Ac) = result(i, a); + } + all_fun::commu(com_mod, gResult); + for (int a = 0; a < lFa.nNo; a++) { + int Ac = lFa.gN(a); + for (int i = 0; i < nsd; i++) + result(i, a) = gResult(i, Ac); + } + + return result; +} + }; diff --git a/Code/Source/solver/post.h b/Code/Source/solver/post.h index b40daae87..14abb303c 100644 --- a/Code/Source/solver/post.h +++ b/Code/Source/solver/post.h @@ -35,6 +35,16 @@ void shl_post(Simulation* simulation, const mshType& lM, const int m, Array& res, Vector& resE, const SolutionStates& solutions, const int iEq, consts::OutputNameType outGrp); +/// @brief Compute consistent nodal traction forces at a fluid face. +/// +/// Used by partitioned FSI to extract the fluid traction at the FSI +/// interface. Returns force ON the solid (sign: -(sigma . n_fluid)). +Array compute_face_traction( + ComMod& com_mod, const CmMod& cm_mod, + const mshType& fluid_mesh, const faceType& fluid_face, + const eqType& fluid_eq, + const SolutionStates& solutions); + }; #endif diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 65b9aaaf1..e4c986844 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -1807,12 +1807,18 @@ void read_files(Simulation* simulation, const std::string& file_name) } } - if (eq.phys == EquationType::phys_mesh) { + if (eq.phys == EquationType::phys_mesh) { + // For partitioned FSI, mvMsh is set when Partitioned_coupling is configured + if (!com_mod.mvMsh && simulation->parameters.partitioned_coupling_parameters.defined()) { + com_mod.mvMsh = true; + } if (!com_mod.mvMsh) { - throw std::runtime_error("mesh equation can only be specified after FSI equation"); + throw std::runtime_error("mesh equation can only be specified after FSI or with Partitioned_coupling"); + } + if (com_mod.nEq > 0 && com_mod.eq[0].phys == EquationType::phys_FSI) { + // Use the explicit geometry coupling flag of the FSI equation. + eq.expl_geom_cpl = com_mod.eq[0].expl_geom_cpl; } - // Use the explicit geometry coupling flag of the FSI equation. - eq.expl_geom_cpl = com_mod.eq[0].expl_geom_cpl; } } #ifdef debug_read_files diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 5277f6009..87461f454 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -1109,6 +1109,9 @@ void set_bc_dir_l(ComMod& com_mod, const bcType& lBc, const faceType& lFa, Array // void set_bc_dir_w(ComMod& com_mod, const SolutionStates& solutions) { + // Local alias for old displacement + const auto& Do = solutions.old.get_displacement(); + using namespace consts; const int cEq = com_mod.cEq; @@ -1231,8 +1234,12 @@ void set_bc_dir_wl(ComMod& com_mod, const bcType& lBc, const mshType& lM, const } } - Vector ptr(eNoN); - Array xl(nsd,eNoN), yl(tDof,eNoN), lR(dof,eNoN); + // For ALE partitioned FSI, extend yl to hold mesh velocity + const bool has_ale = (com_mod.ale_mesh_velocity.size() > 0); + const int yl_nrows = has_ale ? tDof + nsd : tDof; + + Vector ptr(eNoN); + Array xl(nsd,eNoN), yl(yl_nrows,eNoN), lR(dof,eNoN); Array3 lK(dof*dof,eNoN,eNoN); Array xbl(nsd,eNoNb), ubl(nsd,eNoNb); @@ -1261,6 +1268,11 @@ void set_bc_dir_wl(ComMod& com_mod, const bcType& lBc, const mshType& lM, const for (int i = 0; i < tDof; i++) { yl(i,a) = Yg(i,Ac); } + if (has_ale) { + for (int i = 0; i < nsd; i++) { + yl(nsd+1+i, a) = com_mod.ale_mesh_velocity(i, Ac); + } + } for (int i = 0; i < nsd; i++) { xl(i,a) = com_mod.x(i,Ac); @@ -1991,6 +2003,71 @@ void set_bc_undef_neu_l(ComMod& com_mod, const bcType& lBc, const faceType& lFa) } } +//---------------------------------------------------------------------- +// enforce_dirichlet_on_face +//---------------------------------------------------------------------- +void enforce_dirichlet_on_face(ComMod& com_mod, const faceType& lFa, int nsd) +{ + const auto& eq = com_mod.eq[com_mod.cEq]; + const int dof = eq.dof; + const auto& rowPtr = com_mod.rowPtr; + const auto& colPtr = com_mod.colPtr; + auto& R = com_mod.R; + auto& Val = com_mod.Val; + + for (int a = 0; a < lFa.nNo; a++) { + int rowN = lFa.gN(a); + for (int i = 0; i < dof; i++) { + R(i, rowN) = 0.0; + } + for (int j = rowPtr(rowN); j <= rowPtr(rowN + 1) - 1; j++) { + int colN = colPtr(j); + for (int iDof = 0; iDof < dof * dof; iDof++) { + Val(iDof, j) = 0.0; + } + if (colN == rowN) { + for (int i = 0; i < dof; i++) { + Val(i * dof + i, j) = 1.0; + } + } + } + } +} + +//---------------------------------------------------------------------- +// enforce_dirichlet_dofs_on_face +//---------------------------------------------------------------------- +void enforce_dirichlet_dofs_on_face(ComMod& com_mod, const faceType& lFa, + int dof_start, int num_dofs) +{ + const auto& eq = com_mod.eq[com_mod.cEq]; + const int dof = eq.dof; + const auto& rowPtr = com_mod.rowPtr; + const auto& colPtr = com_mod.colPtr; + auto& R = com_mod.R; + auto& Val = com_mod.Val; + + for (int a = 0; a < lFa.nNo; a++) { + int rowN = lFa.gN(a); + for (int i = dof_start; i < dof_start + num_dofs; i++) { + R(i, rowN) = 0.0; + } + for (int j = rowPtr(rowN); j <= rowPtr(rowN + 1) - 1; j++) { + int colN = colPtr(j); + for (int i = dof_start; i < dof_start + num_dofs; i++) { + for (int k = 0; k < dof; k++) { + Val(i * dof + k, j) = 0.0; + } + } + if (colN == rowN) { + for (int i = dof_start; i < dof_start + num_dofs; i++) { + Val(i * dof + i, j) = 1.0; + } + } + } + } +} + }; diff --git a/Code/Source/solver/set_bc.h b/Code/Source/solver/set_bc.h index b84eadba7..229788d0a 100644 --- a/Code/Source/solver/set_bc.h +++ b/Code/Source/solver/set_bc.h @@ -45,6 +45,13 @@ void set_bc_undef_neu(ComMod& com_mod); void set_bc_undef_neu_l(ComMod& com_mod, const bcType& lBc, const faceType& lFa); +/// @brief Enforce Dirichlet BC at all DOFs of face nodes in assembled system. +void enforce_dirichlet_on_face(ComMod& com_mod, const faceType& lFa, int nsd); + +/// @brief Enforce Dirichlet BC for DOFs [dof_start, dof_start+num_dofs) at face nodes. +void enforce_dirichlet_dofs_on_face(ComMod& com_mod, const faceType& lFa, + int dof_start, int num_dofs); + }; #endif diff --git a/tests/cases/fsi/compare_fsi.py b/tests/cases/fsi/compare_fsi.py new file mode 100644 index 000000000..2a03dafb4 --- /dev/null +++ b/tests/cases/fsi/compare_fsi.py @@ -0,0 +1,215 @@ +""" +Compare monolithic vs partitioned FSI results and generate: +1. Field comparison at final time step +2. Videos of both simulations +3. Coupling performance plot for partitioned FSI +""" + +import numpy as np +import meshio +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from matplotlib.gridspec import GridSpec +from matplotlib.colors import Normalize +import matplotlib.animation as animation +import os, re + +mono_dir = "pipe_3d/1-procs" +part_dir = "pipe_3d_partitioned/1-procs" +coupling_log = "pipe_3d_partitioned/coupling_log.txt" +out_dir = "pipe_3d_partitioned" + +n_steps = 50 + +# ============================================================ +# 1. Parse coupling log +# ============================================================ +print("Parsing coupling log...") +coupling_data = {} # ts -> [(outer, omega, rel_disp), ...] + +with open(coupling_log) as f: + for line in f: + # Format: CP TS-ITER TIME [dB REL_DISP OMEGA] + m = re.match(r'\s*CP\s+(\d+)-(\d+)\s+([\d.e+-]+)\s+\[(-?\d+)\s+([\d.e+-]+)\s+([\d.e+-]+)\]', line) + if m: + ts = int(m.group(1)) + outer = int(m.group(2)) + rel_disp = float(m.group(5)) + omega = float(m.group(6)) + if ts not in coupling_data: + coupling_data[ts] = [] + coupling_data[ts].append((outer, omega, rel_disp)) + +# ============================================================ +# 2. Coupling performance plot +# ============================================================ +print("Creating coupling performance plot...") + +fig, axes = plt.subplots(2, 1, figsize=(10, 7), sharex=True) + +# Top: number of coupling iterations per time step +ts_list = sorted(coupling_data.keys()) +n_iters = [len(coupling_data[ts]) for ts in ts_list] +axes[0].bar(ts_list, n_iters, color='steelblue', width=0.8) +axes[0].set_ylabel('Coupling iterations') +axes[0].set_title('Partitioned FSI Coupling Performance (Aitken relaxation)') +axes[0].set_ylim(0, max(n_iters) + 1) + +# Bottom: convergence history (all time steps overlaid) +cmap = plt.cm.viridis +for i, ts in enumerate(ts_list): + iters = coupling_data[ts] + x = [it[0] for it in iters] + y = [it[2] for it in iters] + color = cmap(i / max(len(ts_list) - 1, 1)) + axes[1].semilogy(x, y, 'o-', color=color, alpha=0.6, lw=1, markersize=4) + +# Highlight first and last +for ts, color, label in [(ts_list[0], 'blue', f'TS {ts_list[0]}'), + (ts_list[-1], 'red', f'TS {ts_list[-1]}')]: + iters = coupling_data[ts] + x = [it[0] for it in iters] + y = [it[2] for it in iters] + axes[1].semilogy(x, y, 'o-', color=color, lw=2, markersize=6, label=label) + +axes[1].axhline(1e-8, color='green', linestyle='--', linewidth=1.5, label='Tolerance (1e-8)') +axes[1].set_xlabel('Coupling iteration within time step') +axes[1].set_ylabel('Relative displacement change') +axes[1].legend(loc='upper right') +axes[1].set_ylim(1e-12, 10) +axes[1].set_xlim(0.5, max(n_iters) + 0.5) + +plt.tight_layout() +plt.savefig(os.path.join(out_dir, 'coupling_performance.png'), dpi=150) +plt.close() +print(f" Saved {out_dir}/coupling_performance.png") + +# ============================================================ +# 3. Final time step comparison +# ============================================================ +print("Comparing final time step fields...") + +mono = meshio.read(os.path.join(mono_dir, f"result_{n_steps:03d}.vtu")) +part = meshio.read(os.path.join(part_dir, f"result_{n_steps:03d}.vtu")) + +print(f"\n{'Field':<20} {'Mono max':>12} {'Part max':>12} {'Rel diff':>12}") +print("-" * 60) +for f in mono.point_data: + m = np.max(np.abs(mono.point_data[f])) + if f in part.point_data: + p = np.max(np.abs(part.point_data[f])) + else: + p = 0.0 + rd = abs(m - p) / (m + 1e-30) + print(f"{f:<20} {m:12.4e} {p:12.4e} {rd:12.4e}") + +# ============================================================ +# 4. Videos (side-by-side velocity magnitude on z=0 slice) +# ============================================================ +print("\nCreating videos...") + +def get_velocity_mag(fname): + """Read VTU and return (points, velocity_magnitude)""" + m = meshio.read(fname) + pts = m.points + vel = m.point_data.get('Velocity', np.zeros((len(pts), 3))) + vmag = np.sqrt(vel[:, 0]**2 + vel[:, 1]**2 + vel[:, 2]**2) + return pts, vmag + +def get_displacement_mag(fname): + """Read VTU and return (points, displacement_magnitude)""" + m = meshio.read(fname) + pts = m.points + # Try FS_Displacement first (monolithic), then Displacement + for key in ['FS_Displacement', 'Displacement']: + if key in m.point_data: + d = m.point_data[key] + if np.max(np.abs(d)) > 1e-15: + return pts, np.sqrt(d[:, 0]**2 + d[:, 1]**2 + d[:, 2]**2) + return pts, np.zeros(len(pts)) + +# Get all data for velocity video +print(" Reading velocity data...") +mono_vel = [] +part_vel = [] +for ts in range(1, n_steps + 1): + mono_pts, mono_vmag = get_velocity_mag(os.path.join(mono_dir, f"result_{ts:03d}.vtu")) + part_pts, part_vmag = get_velocity_mag(os.path.join(part_dir, f"result_{ts:03d}.vtu")) + mono_vel.append((mono_pts, mono_vmag)) + part_vel.append((part_pts, part_vmag)) + +# Use monolithic velocity range for color scaling (partitioned may differ) +vmax_mono = max(np.max(v[1]) for v in mono_vel) + +# Create velocity animation +print(" Rendering velocity video...") +fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + +def plot_frame(frame_idx): + for ax in axes: + ax.clear() + + pts_m, vmag_m = mono_vel[frame_idx] + pts_p, vmag_p = part_vel[frame_idx] + + # Each subplot uses its own normalization for best visibility + sc1 = axes[0].scatter(pts_m[:, 2], pts_m[:, 0], c=vmag_m, s=2, + vmin=0, vmax=vmax_mono, cmap='coolwarm') + axes[0].set_title(f'Monolithic FSI') + axes[0].set_xlabel('z') + axes[0].set_ylabel('x') + axes[0].set_aspect('equal') + + sc2 = axes[1].scatter(pts_p[:, 2], pts_p[:, 0], c=vmag_p, s=2, + vmin=0, vmax=vmax_mono, cmap='coolwarm') + axes[1].set_title(f'Partitioned FSI') + axes[1].set_xlabel('z') + axes[1].set_ylabel('x') + axes[1].set_aspect('equal') + + fig.suptitle(f'Velocity Magnitude (step {frame_idx+1}/{n_steps}, dt=1e-4)', fontsize=14) + return sc1, sc2 + +plot_frame(0) +plt.tight_layout() + +ani = animation.FuncAnimation(fig, plot_frame, frames=n_steps, interval=200, blit=False) +ani.save(os.path.join(out_dir, 'velocity_comparison.gif'), writer='pillow', fps=5) +plt.close() +print(f" Saved {out_dir}/velocity_comparison.gif") + +# Create pressure comparison at final step +print(" Creating final pressure comparison...") +fig, axes = plt.subplots(1, 2, figsize=(14, 5)) + +mono_final = meshio.read(os.path.join(mono_dir, f"result_{n_steps:03d}.vtu")) +part_final = meshio.read(os.path.join(part_dir, f"result_{n_steps:03d}.vtu")) + +p_mono = mono_final.point_data.get('Pressure', np.zeros(len(mono_final.points))) +p_part = part_final.point_data.get('Pressure', np.zeros(len(part_final.points))) +pmax = max(np.max(np.abs(p_mono)), np.max(np.abs(p_part))) + +sc1 = axes[0].scatter(mono_final.points[:, 2], mono_final.points[:, 0], + c=p_mono, s=2, vmin=-pmax, vmax=pmax, cmap='RdBu_r') +axes[0].set_title('Monolithic FSI') +axes[0].set_xlabel('z') +axes[0].set_ylabel('x') +axes[0].set_aspect('equal') +plt.colorbar(sc1, ax=axes[0], label='Pressure') + +sc2 = axes[1].scatter(part_final.points[:, 2], part_final.points[:, 0], + c=p_part, s=2, vmin=-pmax, vmax=pmax, cmap='RdBu_r') +axes[1].set_title('Partitioned FSI') +axes[1].set_xlabel('z') +axes[1].set_ylabel('x') +axes[1].set_aspect('equal') +plt.colorbar(sc2, ax=axes[1], label='Pressure') + +fig.suptitle(f'Pressure at step {n_steps}', fontsize=14) +plt.tight_layout() +plt.savefig(os.path.join(out_dir, 'pressure_comparison.png'), dpi=150) +plt.close() +print(f" Saved {out_dir}/pressure_comparison.png") + +print("\nDone!") diff --git a/tests/cases/fsi/pipe_3d/solver_10step.xml b/tests/cases/fsi/pipe_3d/solver_10step.xml new file mode 100644 index 000000000..c72567382 --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_10step.xml @@ -0,0 +1,156 @@ + + + + + 0 + 3 + 10 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + Neu + 5.0e4 + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1 ) + + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d/solver_1step.xml b/tests/cases/fsi/pipe_3d/solver_1step.xml new file mode 100644 index 000000000..aea35f330 --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_1step.xml @@ -0,0 +1,156 @@ + + + + + 0 + 3 + 1 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + Neu + 5.0e4 + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1 ) + + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d/solver_50.xml b/tests/cases/fsi/pipe_3d/solver_50.xml new file mode 100644 index 000000000..10697bda7 --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_50.xml @@ -0,0 +1,156 @@ + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + Neu + 5.0e4 + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1 ) + + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d/solver_ramp.xml b/tests/cases/fsi/pipe_3d/solver_ramp.xml new file mode 100644 index 000000000..f15a06aac --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_ramp.xml @@ -0,0 +1,158 @@ + + + + + + + 0 + 3 + 10 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 10 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + + Neu + Unsteady + ../pipe_3d_partitioned/inlet_pressure_ramp.dat + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d/solver_stiff.xml b/tests/cases/fsi/pipe_3d/solver_stiff.xml new file mode 100644 index 000000000..e21c524dc --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_stiff.xml @@ -0,0 +1,156 @@ + + + + + 0 + 3 + 1 + 1e-4 + 0.50 + STOP_SIM + true + result + 5 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e12 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + Neu + 5.0e4 + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1 ) + + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d/solver_stiff_save.xml b/tests/cases/fsi/pipe_3d/solver_stiff_save.xml new file mode 100644 index 000000000..534a4e724 --- /dev/null +++ b/tests/cases/fsi/pipe_3d/solver_stiff_save.xml @@ -0,0 +1,156 @@ + + + + + 0 + 3 + 1 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + true + 1 + 7 + 1e-12 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + struct + + M94 + 1.0 + 1.0e12 + 0.3 + + + + + fsils + + 1e-12 + 100 + 50 + + + + true + true + true + true + + + + FS_Displacement + + + + Neu + 5.0e4 + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1 ) + + + + + + + true + 1 + 7 + 1e-12 + 0.3 + + + + fsils + + 1e-12 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/README.md b/tests/cases/fsi/pipe_3d_partitioned/README.md new file mode 100755 index 000000000..cee6bff10 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/README.md @@ -0,0 +1,19 @@ + +# **Problem Description** + +Simulate pressure wave propagation in an arterial model using the Arbitrary Lagrangian-Eulerian method [1]. The problem set-up is as follows. + +

+ +

+ +And the results are + +

+ +

+ + +## References + +1. Liu, Ju, and Alison L. Marsden. A Unified Continuum and Variational Multiscale Formulation for Fluids, Solids, and Fluid Structure Interaction. *Computer Methods in Applied Mechanics and Engineering* 337 (August 2018): 549 97. https://doi.org/10.1016/j.cma.2018.03.045. diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_disp_inlet.pdf b/tests/cases/fsi/pipe_3d_partitioned/compare_disp_inlet.pdf new file mode 100644 index 000000000..a72249516 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/compare_disp_inlet.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_disp_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/compare_disp_z.pdf new file mode 100644 index 000000000..4f1cd6cbd Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/compare_disp_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_flow_rate.pdf b/tests/cases/fsi/pipe_3d_partitioned/compare_flow_rate.pdf new file mode 100644 index 000000000..c25c12d87 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/compare_flow_rate.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_fsi.py b/tests/cases/fsi/pipe_3d_partitioned/compare_fsi.py new file mode 100644 index 000000000..309f86aef --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/compare_fsi.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +"""Compare monolithic vs partitioned FSI results. + +Usage: + python compare_fsi.py [--step STEP] [--mono DIR] [--part DIR] + +Plots: + 1. Flow rate at inlet over time + 2. Radial displacement at solid inlet face over time + 3. Radial displacement of solid along z-axis at given time step + 4. Centerline pressure along z-axis at given time step + 5. Centerline velocity along z-axis at given time step +""" + +import argparse +import glob +import os +import numpy as np +import matplotlib.pyplot as plt + +try: + import meshio +except ImportError: + import subprocess + subprocess.check_call(["pip3", "install", "meshio", "-q"]) + import meshio + + +def find_result_files(result_dir, prefix): + """Find all result VTU files sorted by step number.""" + pattern = os.path.join(result_dir, f"{prefix}_*.vtu") + files = sorted(glob.glob(pattern), key=lambda f: int(f.split("_")[-1].split(".")[0])) + return files + + +def get_step_number(filename): + return int(filename.split("_")[-1].split(".")[0]) + + +def read_mesh(filename): + return meshio.read(filename) + + +def compute_centerline_velocity_at_inlet(mesh): + """Axial velocity at the centerline (r ≈ 0) at the inlet face (z ≈ z_min).""" + pts = mesh.points + vel = mesh.point_data.get("Velocity", None) + if vel is None: + return 0.0 + + # Find inlet nodes (z ≈ z_min) + z = pts[:, 2] + z_min = z.min() + inlet_mask = np.abs(z - z_min) < 1e-6 * (z.max() - z.min() + 1e-30) + + if inlet_mask.sum() == 0: + return 0.0 + + # Exclude solid nodes using Domain_ID if available + domain_id = mesh.point_data.get("Domain_ID", None) + if domain_id is not None: + fluid_mask = domain_id[inlet_mask].flatten() == 1 + else: + fluid_mask = np.ones(inlet_mask.sum(), dtype=bool) + + # Find centerline nodes (r ≈ 0) among fluid inlet nodes + inlet_pts = pts[inlet_mask][fluid_mask] + inlet_vel = vel[inlet_mask][fluid_mask] + r = np.sqrt(inlet_pts[:, 0]**2 + inlet_pts[:, 1]**2) + r_max = r.max() if len(r) > 0 else 1.0 + cl_mask = r < 0.1 * r_max + + v_axial = inlet_vel[cl_mask, 2] + if len(v_axial) == 0: + return 0.0 + return np.mean(v_axial) + + +def compute_radial_disp_at_inlet(mesh, disp_key="Displacement"): + """Mean radial displacement of the outermost ring at the inlet face.""" + pts = mesh.points + disp = mesh.point_data.get(disp_key, None) + if disp is None: + return 0.0 + + z = pts[:, 2] + z_min = z.min() + z_tol = 1e-6 * (z.max() - z.min() + 1e-30) + inlet_mask = np.abs(z - z_min) < z_tol + + if inlet_mask.sum() == 0: + return 0.0 + + # Find the outermost solid ring at the inlet face + domain_id = mesh.point_data.get("Domain_ID", None) + r = np.sqrt(pts[inlet_mask, 0]**2 + pts[inlet_mask, 1]**2) + if domain_id is not None: + # Use Domain_ID to select solid nodes (2), then take outermost ring + solid_mask = domain_id[inlet_mask].flatten() == 2 + if solid_mask.sum() == 0: + return 0.0 + r_solid = r.copy() + r_solid[~solid_mask] = 0.0 + r_max = r_solid.max() + else: + r_max = r.max() + outer_ring = r > 0.9 * r_max # outermost 10% of radial range + + x = pts[inlet_mask, 0][outer_ring] + y = pts[inlet_mask, 1][outer_ring] + dx = disp[inlet_mask, 0][outer_ring] + dy = disp[inlet_mask, 1][outer_ring] + rr = np.sqrt(x**2 + y**2) + rr[rr < 1e-30] = 1e-30 + radial = (x * dx + y * dy) / rr + return np.mean(radial) + + +def extract_centerline(mesh, field_name, nsd=3): + """Extract field values along the centerline (r ≈ 0).""" + pts = mesh.points + data = mesh.point_data.get(field_name, None) + if data is None: + return np.array([]), np.array([]) + + r = np.sqrt(pts[:, 0]**2 + pts[:, 1]**2) + # For monolithic: only use fluid nodes for centerline + domain_id = mesh.point_data.get("Domain_ID", None) + if domain_id is not None: + fluid_r = r.copy() + fluid_r[domain_id.flatten() != 1] = np.inf + r_max = r[domain_id.flatten() == 1].max() if (domain_id.flatten() == 1).any() else r.max() + cl_mask = (fluid_r < 0.1 * r_max) + else: + r_max = r.max() + cl_mask = r < 0.1 * r_max # within 10% of center + + if cl_mask.sum() == 0: + return np.array([]), np.array([]) + + z = pts[cl_mask, 2] + vals = data[cl_mask] + order = np.argsort(z) + return z[order], vals[order] + + +def extract_solid_radial_disp_along_z(mesh, disp_key="Displacement"): + """Extract radial displacement along z at the outer wall ring.""" + pts = mesh.points + disp = mesh.point_data.get(disp_key, None) + if disp is None: + return np.array([]), np.array([]) + + # Find outermost solid nodes + domain_id = mesh.point_data.get("Domain_ID", None) + r = np.sqrt(pts[:, 0]**2 + pts[:, 1]**2) + if domain_id is not None: + solid_mask = domain_id.flatten() == 2 + r_solid = r.copy() + r_solid[~solid_mask] = 0.0 + r_max = r_solid.max() + else: + r_max = r.max() + inner_mask = r > 0.9 * r_max + + if inner_mask.sum() == 0: + return np.array([]), np.array([]) + + z = pts[inner_mask, 2] + x, y = pts[inner_mask, 0], pts[inner_mask, 1] + dx, dy = disp[inner_mask, 0], disp[inner_mask, 1] + rr = np.sqrt(x**2 + y**2) + rr[rr < 1e-30] = 1e-30 + radial = (x * dx + y * dy) / rr + + # Average over circumference at each z + z_unique = np.unique(np.round(z, 8)) + z_avg = [] + d_avg = [] + for zz in sorted(z_unique): + mask = np.abs(z - zz) < 1e-6 + z_avg.append(zz) + d_avg.append(np.mean(radial[mask])) + + return np.array(z_avg), np.array(d_avg) + + +def main(): + parser = argparse.ArgumentParser(description="Compare monolithic vs partitioned FSI") + parser.add_argument("--step", type=int, default=5, help="Time step for z-axis plots") + parser.add_argument("--mono", default="../pipe_3d/1-procs", help="Monolithic results directory") + parser.add_argument("--part", default="1-procs", help="Partitioned results directory") + parser.add_argument("--dt", type=float, default=1e-4, help="Time step size") + args = parser.parse_args() + + plt.rcParams.update({"font.size": 10, "figure.figsize": (8, 5)}) + + # Find result files + mono_files = find_result_files(args.mono, "result") + part_fluid_files = find_result_files(args.part, "result_fluid") + part_solid_files = find_result_files(args.part, "result_solid") + + if not mono_files: + print(f"No monolithic results found in {args.mono}") + return + if not part_fluid_files: + print(f"No partitioned fluid results found in {args.part}") + return + + print(f"Monolithic: {len(mono_files)} steps") + print(f"Partitioned: {len(part_fluid_files)} fluid, {len(part_solid_files)} solid steps") + + # ---- Time history plots ---- + n_steps = min(len(mono_files), len(part_fluid_files)) + steps = [] + mono_vel, part_vel = [], [] + mono_disp_inlet, part_disp_inlet = [], [] + + for i in range(n_steps): + step = get_step_number(mono_files[i]) + steps.append(step) + + m = read_mesh(mono_files[i]) + mono_vel.append(compute_centerline_velocity_at_inlet(m)) + mono_disp_inlet.append(compute_radial_disp_at_inlet(m, "FS_Displacement")) + + pf = read_mesh(part_fluid_files[i]) + part_vel.append(compute_centerline_velocity_at_inlet(pf)) + + if i < len(part_solid_files): + ps = read_mesh(part_solid_files[i]) + part_disp_inlet.append(compute_radial_disp_at_inlet(ps, "Displacement")) + else: + part_disp_inlet.append(0.0) + + time = np.array(steps) * args.dt + + # Plot 1: Centerline axial velocity at inlet + fig, ax = plt.subplots() + ax.plot(time * 1e3, mono_vel, "b-o", ms=3, label="Monolithic") + ax.plot(time * 1e3, part_vel, "r-s", ms=3, label="Partitioned") + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Centerline axial velocity") + ax.set_title("Centerline axial velocity at inlet") + ax.legend() + ax.grid(True, alpha=0.3) + fig.tight_layout() + fig.savefig("compare_flow_rate.pdf") + print("Saved compare_flow_rate.pdf") + + # Plot 2: Radial displacement at solid inlet + fig, ax = plt.subplots() + ax.plot(time * 1e3, mono_disp_inlet, "b-o", ms=3, label="Monolithic") + ax.plot(time * 1e3, part_disp_inlet, "r-s", ms=3, label="Partitioned") + ax.set_xlabel("Time [ms]") + ax.set_ylabel("Mean radial displacement at inlet") + ax.set_title("Solid displacement at inlet face") + ax.legend() + ax.grid(True, alpha=0.3) + fig.tight_layout() + fig.savefig("compare_disp_inlet.pdf") + print("Saved compare_disp_inlet.pdf") + + # ---- Spatial plots at given time step ---- + step = args.step + mono_file = os.path.join(args.mono, f"result_{step:03d}.vtu") + part_fluid_file = os.path.join(args.part, f"result_fluid_{step:03d}.vtu") + part_solid_file = os.path.join(args.part, f"result_solid_{step:03d}.vtu") + + if not os.path.exists(mono_file): + print(f"Monolithic result not found: {mono_file}") + return + if not os.path.exists(part_fluid_file): + print(f"Partitioned fluid result not found: {part_fluid_file}") + return + + m = read_mesh(mono_file) + + # Plot 3: Radial displacement along z + fig, ax = plt.subplots() + mz, md = extract_solid_radial_disp_along_z(m, "FS_Displacement") + ax.plot(mz, md, "b-o", ms=3, label="Monolithic") + if os.path.exists(part_solid_file): + ps = read_mesh(part_solid_file) + pz, pd = extract_solid_radial_disp_along_z(ps, "Displacement") + ax.plot(pz, pd, "r-s", ms=3, label="Partitioned") + ax.set_xlabel("z") + ax.set_ylabel("Radial displacement") + ax.set_title(f"Solid radial displacement along z (step {step})") + ax.legend() + ax.grid(True, alpha=0.3) + fig.tight_layout() + fig.savefig("compare_disp_z.pdf") + print("Saved compare_disp_z.pdf") + + # Plot 4: Centerline pressure + fig, ax = plt.subplots() + mz, mp = extract_centerline(m, "Pressure") + pf = read_mesh(part_fluid_file) + pz, pp = extract_centerline(pf, "Pressure") + if len(mz) > 0: + ax.plot(mz, mp, "b-o", ms=3, label="Monolithic") + if len(pz) > 0: + ax.plot(pz, pp, "r-s", ms=3, label="Partitioned") + ax.set_xlabel("z") + ax.set_ylabel("Pressure") + ax.set_title(f"Centerline pressure (step {step})") + ax.legend() + ax.grid(True, alpha=0.3) + fig.tight_layout() + fig.savefig("compare_pressure_z.pdf") + print("Saved compare_pressure_z.pdf") + + # Plot 5: Centerline axial velocity + fig, ax = plt.subplots() + mz, mv = extract_centerline(m, "Velocity") + pz, pv = extract_centerline(pf, "Velocity") + if len(mz) > 0: + ax.plot(mz, mv[:, 2] if mv.ndim > 1 else mv, "b-o", ms=3, label="Monolithic") + if len(pz) > 0: + ax.plot(pz, pv[:, 2] if pv.ndim > 1 else pv, "r-s", ms=3, label="Partitioned") + ax.set_xlabel("z") + ax.set_ylabel("Axial velocity") + ax.set_title(f"Centerline axial velocity (step {step})") + ax.legend() + ax.grid(True, alpha=0.3) + fig.tight_layout() + fig.savefig("compare_velocity_z.pdf") + print("Saved compare_velocity_z.pdf") + + plt.close("all") + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_pressure_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/compare_pressure_z.pdf new file mode 100644 index 000000000..ff892cfde Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/compare_pressure_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/compare_velocity_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/compare_velocity_z.pdf new file mode 100644 index 000000000..be3b81e33 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/compare_velocity_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/configuration.png b/tests/cases/fsi/pipe_3d_partitioned/configuration.png new file mode 100644 index 000000000..47d287aa2 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/configuration.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c6f4038337823c1c6ffca373eaeb737a9f7c74de3225b1021d7178608c63798 +size 679523 diff --git a/tests/cases/fsi/pipe_3d_partitioned/coupling_log.txt b/tests/cases/fsi/pipe_3d_partitioned/coupling_log.txt new file mode 100644 index 000000000..13d67a9a6 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/coupling_log.txt @@ -0,0 +1,902 @@ + CP 1-1 1.262e+00 [0 1.000e+00 1.000e+00] + CP 1-2 2.281e+00 [-9 1.120e-01 1.119e+00] + CP 1-3 3.079e+00 [-20 9.251e-03 1.180e+00] + CP 1-4 3.825e+00 [-25 2.953e-03 1.591e+00] + CP 1-5 4.589e+00 [-31 7.739e-04 1.273e+00] + CP 1-6 5.228e+00 [-33 4.444e-04 8.173e-01] + CP 1-7 5.863e+00 [-38 1.479e-04 1.201e+00] + CP 1-8 6.644e+00 [-48 1.328e-05 1.109e+00] + CP 1-9 7.551e+00 [-52 6.076e-06 7.798e-01] + CP 1-10 8.213e+00 [-55 2.672e-06 1.373e+00] + CP 1-11 8.889e+00 [-61 6.349e-07 1.110e+00] + CP 1-12 9.719e+00 [-67 1.700e-07 8.777e-01] + CP 1-13 1.051e+01 [-73 4.897e-08 1.228e+00] + CP 1-14 1.125e+01 [-84 3.452e-09 1.150e+00] + CP 1-15 1.248e+01 [-85 2.707e-09 6.467e-01] + CP 1-16 1.774e+01 [-88 1.559e-09 1.521e+00] + CP 1-17 2.305e+01 [-91 6.655e-10 1.066e+00] + CP 1-18 2.842e+01 [-98 1.562e-10 8.824e-01] + CP 1-19 3.372e+01 [-102 6.061e-11 1.410e+00] + CP 2-1 3.473e+01 [-6 2.241e-01 1.000e+00] + CP 2-2 3.573e+01 [-16 2.004e-02 1.084e+00] + CP 2-3 3.653e+01 [-29 1.094e-03 1.117e+00] + CP 2-4 3.732e+01 [-35 2.751e-04 1.394e+00] + CP 2-5 3.812e+01 [-43 4.728e-05 1.206e+00] + CP 2-6 3.889e+01 [-46 2.116e-05 8.520e-01] + CP 2-7 3.963e+01 [-52 5.965e-06 1.160e+00] + CP 2-8 4.039e+01 [-63 4.110e-07 1.100e+00] + CP 2-9 4.119e+01 [-67 1.822e-07 8.131e-01] + CP 2-10 4.190e+01 [-71 7.361e-08 1.331e+00] + CP 2-11 4.285e+01 [-78 1.459e-08 1.113e+00] + CP 2-12 4.365e+01 [-83 4.249e-09 8.689e-01] + CP 2-13 4.906e+01 [-88 1.291e-09 1.233e+00] + CP 2-14 5.442e+01 [-99 1.077e-10 1.144e+00] + CP 2-15 5.994e+01 [-101 7.041e-11 7.016e-01] + CP 3-1 6.100e+01 [-6 2.028e-01 1.000e+00] + CP 3-2 6.187e+01 [-15 2.970e-02 1.138e+00] + CP 3-3 6.281e+01 [-26 2.263e-03 1.194e+00] + CP 3-4 6.360e+01 [-30 9.190e-04 1.665e+00] + CP 3-5 6.433e+01 [-34 3.229e-04 1.239e+00] + CP 3-6 6.518e+01 [-38 1.469e-04 8.581e-01] + CP 3-7 6.590e+01 [-43 4.039e-05 1.166e+00] + CP 3-8 6.676e+01 [-56 2.184e-06 1.122e+00] + CP 3-9 6.749e+01 [-58 1.571e-06 6.792e-01] + CP 3-10 6.833e+01 [-60 8.906e-07 1.535e+00] + CP 3-11 6.917e+01 [-64 3.718e-07 1.083e+00] + CP 3-12 7.001e+01 [-72 5.979e-08 9.341e-01] + CP 3-13 7.097e+01 [-79 1.205e-08 1.166e+00] + CP 3-14 7.178e+01 [-94 3.611e-10 1.191e+00] + CP 3-15 7.393e+01 [-92 5.815e-10 2.725e-01] + CP 3-16 7.927e+01 [-92 6.010e-10 2.000e+00] + CP 3-17 8.463e+01 [-92 5.509e-10 1.044e+00] + CP 3-18 9.009e+01 [-104 3.805e-11 9.762e-01] + CP 4-1 9.117e+01 [-8 1.294e-01 1.000e+00] + CP 4-2 9.204e+01 [-17 1.942e-02 1.131e+00] + CP 4-3 9.296e+01 [-27 1.809e-03 1.218e+00] + CP 4-4 9.375e+01 [-32 5.942e-04 1.722e+00] + CP 4-5 9.456e+01 [-36 2.030e-04 1.291e+00] + CP 4-6 9.539e+01 [-39 1.150e-04 8.285e-01] + CP 4-7 9.617e+01 [-44 3.537e-05 1.181e+00] + CP 4-8 9.695e+01 [-55 2.534e-06 1.110e+00] + CP 4-9 9.778e+01 [-58 1.379e-06 7.383e-01] + CP 4-10 9.865e+01 [-61 6.785e-07 1.434e+00] + CP 4-11 9.953e+01 [-66 2.078e-07 1.098e+00] + CP 4-12 1.004e+02 [-73 4.445e-08 9.060e-01] + CP 4-13 1.014e+02 [-79 1.096e-08 1.199e+00] + CP 4-14 1.023e+02 [-94 3.495e-10 1.168e+00] + CP 4-15 1.077e+02 [-92 5.781e-10 4.208e-01] + CP 4-16 1.131e+02 [-93 4.848e-10 2.000e+00] + CP 4-17 1.187e+02 [-93 4.425e-10 1.046e+00] + CP 4-18 1.240e+02 [-104 3.270e-11 9.740e-01] + CP 5-1 1.251e+02 [-6 2.330e-01 1.000e+00] + CP 5-2 1.261e+02 [-15 2.764e-02 1.112e+00] + CP 5-3 1.269e+02 [-26 2.451e-03 1.184e+00] + CP 5-4 1.277e+02 [-31 7.004e-04 1.582e+00] + CP 5-5 1.285e+02 [-37 1.631e-04 1.299e+00] + CP 5-6 1.295e+02 [-39 1.072e-04 7.920e-01] + CP 5-7 1.302e+02 [-44 3.899e-05 1.219e+00] + CP 5-8 1.315e+02 [-53 4.355e-06 1.102e+00] + CP 5-9 1.323e+02 [-57 1.608e-06 8.191e-01] + CP 5-10 1.331e+02 [-62 6.251e-07 1.324e+00] + CP 5-11 1.339e+02 [-69 1.120e-07 1.124e+00] + CP 5-12 1.349e+02 [-74 3.889e-08 8.371e-01] + CP 5-13 1.358e+02 [-78 1.339e-08 1.271e+00] + CP 5-14 1.369e+02 [-87 1.678e-09 1.131e+00] + CP 5-15 1.421e+02 [-91 7.664e-10 7.785e-01] + CP 5-16 1.475e+02 [-94 3.264e-10 1.350e+00] + CP 5-17 1.528e+02 [-101 6.994e-11 1.112e+00] + CP 6-1 1.540e+02 [-5 2.769e-01 1.000e+00] + CP 6-2 1.548e+02 [-14 3.813e-02 1.137e+00] + CP 6-3 1.558e+02 [-25 3.137e-03 1.201e+00] + CP 6-4 1.565e+02 [-29 1.200e-03 1.684e+00] + CP 6-5 1.574e+02 [-33 4.184e-04 1.256e+00] + CP 6-6 1.582e+02 [-36 2.067e-04 8.467e-01] + CP 6-7 1.592e+02 [-42 5.996e-05 1.176e+00] + CP 6-8 1.600e+02 [-54 3.660e-06 1.120e+00] + CP 6-9 1.610e+02 [-56 2.410e-06 6.967e-01] + CP 6-10 1.618e+02 [-58 1.310e-06 1.500e+00] + CP 6-11 1.626e+02 [-63 4.962e-07 1.088e+00] + CP 6-12 1.636e+02 [-70 8.757e-08 9.258e-01] + CP 6-13 1.645e+02 [-77 1.890e-08 1.177e+00] + CP 6-14 1.654e+02 [-94 3.885e-10 1.185e+00] + CP 6-15 1.665e+02 [-90 9.518e-10 1.000e-02] + CP 6-16 1.720e+02 [-89 1.218e-09 3.538e-02] + CP 6-17 1.773e+02 [-89 1.252e-09 1.211e+00] + CP 6-18 1.826e+02 [-96 2.410e-10 1.015e+00] + CP 6-19 1.880e+02 [-109 1.042e-11 9.742e-01] + CP 7-1 1.890e+02 [-8 1.449e-01 1.000e+00] + CP 7-2 1.899e+02 [-17 1.846e-02 1.118e+00] + CP 7-3 1.907e+02 [-28 1.560e-03 1.183e+00] + CP 7-4 1.914e+02 [-33 4.894e-04 1.596e+00] + CP 7-5 1.923e+02 [-38 1.295e-04 1.275e+00] + CP 7-6 1.930e+02 [-41 7.404e-05 8.191e-01] + CP 7-7 1.939e+02 [-46 2.424e-05 1.196e+00] + CP 7-8 1.947e+02 [-56 2.094e-06 1.108e+00] + CP 7-9 1.955e+02 [-60 9.614e-07 7.791e-01] + CP 7-10 1.963e+02 [-63 4.231e-07 1.372e+00] + CP 7-11 1.971e+02 [-69 1.008e-07 1.109e+00] + CP 7-12 1.979e+02 [-75 2.654e-08 8.800e-01] + CP 7-13 1.988e+02 [-81 7.546e-09 1.224e+00] + CP 7-14 1.997e+02 [-92 5.072e-10 1.150e+00] + CP 7-15 2.049e+02 [-93 4.091e-10 6.392e-01] + CP 7-16 2.102e+02 [-96 2.423e-10 1.557e+00] + CP 7-17 2.156e+02 [-99 1.085e-10 1.075e+00] + CP 7-18 2.209e+02 [-107 1.590e-11 9.388e-01] + CP 8-1 2.221e+02 [-6 2.259e-01 1.000e+00] + CP 8-2 2.229e+02 [-14 3.651e-02 1.156e+00] + CP 8-3 2.238e+02 [-25 2.739e-03 1.208e+00] + CP 8-4 2.246e+02 [-28 1.333e-03 1.638e+00] + CP 8-5 2.254e+02 [-33 4.358e-04 1.242e+00] + CP 8-6 2.262e+02 [-36 2.090e-04 8.463e-01] + CP 8-7 2.269e+02 [-42 6.176e-05 1.183e+00] + CP 8-8 2.278e+02 [-53 4.012e-06 1.121e+00] + CP 8-9 2.286e+02 [-55 2.566e-06 7.033e-01] + CP 8-10 2.294e+02 [-58 1.368e-06 1.483e+00] + CP 8-11 2.302e+02 [-63 4.943e-07 1.090e+00] + CP 8-12 2.310e+02 [-70 9.033e-08 9.222e-01] + CP 8-13 2.319e+02 [-76 2.004e-08 1.182e+00] + CP 8-14 2.327e+02 [-94 3.839e-10 1.182e+00] + CP 8-15 2.338e+02 [-89 1.021e-09 1.154e-01] + CP 8-16 2.391e+02 [-89 1.192e-09 6.670e-01] + CP 8-17 2.444e+02 [-93 4.792e-10 1.115e+00] + CP 8-18 2.497e+02 [-104 3.190e-11 1.045e+00] + CP 9-1 2.507e+02 [-8 1.290e-01 1.000e+00] + CP 9-2 2.517e+02 [-18 1.479e-02 1.107e+00] + CP 9-3 2.525e+02 [-29 1.259e-03 1.175e+00] + CP 9-4 2.533e+02 [-34 3.456e-04 1.546e+00] + CP 9-5 2.542e+02 [-41 7.421e-05 1.290e+00] + CP 9-6 2.550e+02 [-43 4.755e-05 7.950e-01] + CP 9-7 2.558e+02 [-47 1.697e-05 1.211e+00] + CP 9-8 2.565e+02 [-57 1.834e-06 1.098e+00] + CP 9-9 2.573e+02 [-61 6.489e-07 8.272e-01] + CP 9-10 2.580e+02 [-66 2.439e-07 1.308e+00] + CP 9-11 2.588e+02 [-73 4.051e-08 1.124e+00] + CP 9-12 2.597e+02 [-78 1.439e-08 8.328e-01] + CP 9-13 2.605e+02 [-82 5.016e-09 1.271e+00] + CP 9-14 2.658e+02 [-91 6.518e-10 1.126e+00] + CP 9-15 2.711e+02 [-95 2.782e-10 7.925e-01] + CP 9-16 2.764e+02 [-99 1.128e-10 1.327e+00] + CP 9-17 2.818e+02 [-106 2.194e-11 1.112e+00] + CP 10-1 2.828e+02 [-5 3.000e-01 1.000e+00] + CP 10-2 2.837e+02 [-13 4.647e-02 1.165e+00] + CP 10-3 2.846e+02 [-24 3.279e-03 1.212e+00] + CP 10-4 2.854e+02 [-27 1.776e-03 1.551e+00] + CP 10-5 2.862e+02 [-33 4.469e-04 1.251e+00] + CP 10-6 2.870e+02 [-36 2.451e-04 8.170e-01] + CP 10-7 2.878e+02 [-40 8.368e-05 1.217e+00] + CP 10-8 2.885e+02 [-50 8.237e-06 1.114e+00] + CP 10-9 2.893e+02 [-54 3.726e-06 7.806e-01] + CP 10-10 2.901e+02 [-57 1.620e-06 1.366e+00] + CP 10-11 2.908e+02 [-64 3.723e-07 1.111e+00] + CP 10-12 2.916e+02 [-69 1.025e-07 8.730e-01] + CP 10-13 2.924e+02 [-75 3.032e-08 1.235e+00] + CP 10-14 2.933e+02 [-86 2.347e-09 1.149e+00] + CP 10-15 2.943e+02 [-87 1.702e-09 6.679e-01] + CP 10-16 2.995e+02 [-90 9.509e-10 1.507e+00] + CP 10-17 3.048e+02 [-94 3.724e-10 1.083e+00] + CP 10-18 3.101e+02 [-102 6.275e-11 9.274e-01] + CP 11-1 3.111e+02 [-14 3.711e-02 1.000e+00] + CP 11-2 3.119e+02 [-22 5.478e-03 1.114e+00] + CP 11-3 3.127e+02 [-31 7.333e-04 1.259e+00] + CP 11-4 3.136e+02 [-38 1.560e-04 1.578e+00] + CP 11-5 3.144e+02 [-47 1.701e-05 1.560e+00] + CP 11-6 3.153e+02 [-46 2.451e-05 5.407e-01] + CP 11-7 3.162e+02 [-47 1.773e-05 1.632e+00] + CP 11-8 3.170e+02 [-50 9.406e-06 1.067e+00] + CP 11-9 3.178e+02 [-59 1.132e-06 9.534e-01] + CP 11-10 3.186e+02 [-67 1.992e-07 1.151e+00] + CP 11-11 3.194e+02 [-79 1.189e-08 1.219e+00] + CP 11-12 3.203e+02 [-80 9.177e-09 2.000e+00] + CP 11-13 3.211e+02 [-82 5.886e-09 1.219e+00] + CP 11-14 3.221e+02 [-86 2.217e-09 8.856e-01] + CP 11-15 3.274e+02 [-92 5.191e-10 1.155e+00] + CP 11-16 3.327e+02 [-109 1.058e-11 1.147e+00] + CP 12-1 3.338e+02 [-7 1.932e-01 1.000e+00] + CP 12-2 3.347e+02 [-14 3.358e-02 1.173e+00] + CP 12-3 3.355e+02 [-26 2.276e-03 1.215e+00] + CP 12-4 3.362e+02 [-28 1.380e-03 1.420e+00] + CP 12-5 3.370e+02 [-36 2.010e-04 1.270e+00] + CP 12-6 3.379e+02 [-38 1.485e-04 7.454e-01] + CP 12-7 3.387e+02 [-41 6.710e-05 1.315e+00] + CP 12-8 3.396e+02 [-48 1.339e-05 1.098e+00] + CP 12-9 3.405e+02 [-54 3.433e-06 8.795e-01] + CP 12-10 3.413e+02 [-59 1.019e-06 1.241e+00] + CP 12-11 3.420e+02 [-70 8.177e-08 1.152e+00] + CP 12-12 3.428e+02 [-72 5.811e-08 6.773e-01] + CP 12-13 3.437e+02 [-74 3.176e-08 1.482e+00] + CP 12-14 3.446e+02 [-79 1.168e-08 1.084e+00] + CP 12-15 3.455e+02 [-86 2.000e-09 9.260e-01] + CP 12-16 3.508e+02 [-93 4.385e-10 1.184e+00] + CP 12-17 3.561e+02 [-112 5.805e-12 1.187e+00] + CP 13-1 3.570e+02 [-8 1.303e-01 1.000e+00] + CP 13-2 3.579e+02 [-16 2.304e-02 1.174e+00] + CP 13-3 3.587e+02 [-27 1.724e-03 1.229e+00] + CP 13-4 3.595e+02 [-30 9.881e-04 1.596e+00] + CP 13-5 3.603e+02 [-35 2.758e-04 1.258e+00] + CP 13-6 3.611e+02 [-38 1.522e-04 8.172e-01] + CP 13-7 3.620e+02 [-42 5.147e-05 1.216e+00] + CP 13-8 3.628e+02 [-53 4.935e-06 1.115e+00] + CP 13-9 3.635e+02 [-56 2.315e-06 7.699e-01] + CP 13-10 3.643e+02 [-59 1.037e-06 1.381e+00] + CP 13-11 3.651e+02 [-65 2.553e-07 1.109e+00] + CP 13-12 3.663e+02 [-71 6.673e-08 8.802e-01] + CP 13-13 3.672e+02 [-77 1.908e-08 1.229e+00] + CP 13-14 3.680e+02 [-88 1.285e-09 1.154e+00] + CP 13-15 3.690e+02 [-89 1.074e-09 6.297e-01] + CP 13-16 3.743e+02 [-91 6.467e-10 1.578e+00] + CP 13-17 3.797e+02 [-95 3.033e-10 1.074e+00] + CP 13-18 3.850e+02 [-103 4.501e-11 9.372e-01] + CP 14-1 3.860e+02 [-5 3.151e-01 1.000e+00] + CP 14-2 3.869e+02 [-12 5.236e-02 1.183e+00] + CP 14-3 3.877e+02 [-24 3.212e-03 1.218e+00] + CP 14-4 3.885e+02 [-26 2.227e-03 1.192e+00] + CP 14-5 3.894e+02 [-35 3.112e-04 1.319e+00] + CP 14-6 3.903e+02 [-40 9.149e-05 1.611e+00] + CP 14-7 3.911e+02 [-46 2.301e-05 1.370e+00] + CP 14-8 3.920e+02 [-47 1.604e-05 8.353e-01] + CP 14-9 3.930e+02 [-52 5.837e-06 1.230e+00] + CP 14-10 3.938e+02 [-62 5.145e-07 1.142e+00] + CP 14-11 3.945e+02 [-65 3.121e-07 7.294e-01] + CP 14-12 3.953e+02 [-68 1.560e-07 1.436e+00] + CP 14-13 3.962e+02 [-73 4.811e-08 1.098e+00] + CP 14-14 3.970e+02 [-79 1.029e-08 9.056e-01] + CP 14-15 3.980e+02 [-85 2.558e-09 1.202e+00] + CP 14-16 4.033e+02 [-100 8.173e-11 1.171e+00] + CP 15-1 4.043e+02 [-7 1.900e-01 1.000e+00] + CP 15-2 4.052e+02 [-14 3.337e-02 1.188e+00] + CP 15-3 4.062e+02 [-26 2.156e-03 1.230e+00] + CP 15-4 4.070e+02 [-28 1.526e-03 1.232e+00] + CP 15-5 4.078e+02 [-37 1.829e-04 1.326e+00] + CP 15-6 4.086e+02 [-40 8.312e-05 1.549e+00] + CP 15-7 4.095e+02 [-47 1.892e-05 1.311e+00] + CP 15-8 4.103e+02 [-48 1.272e-05 8.061e-01] + CP 15-9 4.110e+02 [-53 4.944e-06 1.261e+00] + CP 15-10 4.119e+02 [-62 6.202e-07 1.126e+00] + CP 15-11 4.126e+02 [-65 2.691e-07 7.955e-01] + CP 15-12 4.134e+02 [-69 1.111e-07 1.342e+00] + CP 15-13 4.144e+02 [-76 2.235e-08 1.118e+00] + CP 15-14 4.152e+02 [-81 7.066e-09 8.512e-01] + CP 15-15 4.161e+02 [-86 2.313e-09 1.262e+00] + CP 15-16 4.214e+02 [-96 2.511e-10 1.140e+00] + CP 15-17 4.267e+02 [-98 1.365e-10 7.397e-01] + CP 15-18 4.320e+02 [-101 6.506e-11 1.407e+00] + CP 16-1 4.330e+02 [-8 1.261e-01 1.000e+00] + CP 16-2 4.338e+02 [-16 2.395e-02 1.199e+00] + CP 16-3 4.346e+02 [-28 1.285e-03 1.220e+00] + CP 16-4 4.355e+02 [-29 1.133e-03 7.745e-01] + CP 16-5 4.362e+02 [-32 5.785e-04 1.516e+00] + CP 16-6 4.370e+02 [-36 2.160e-04 1.104e+00] + CP 16-7 4.378e+02 [-43 4.411e-05 9.192e-01] + CP 16-8 4.386e+02 [-50 9.504e-06 1.165e+00] + CP 16-9 4.395e+02 [-65 2.707e-07 1.169e+00] + CP 16-10 4.403e+02 [-63 4.300e-07 2.405e-01] + CP 16-11 4.411e+02 [-63 4.481e-07 2.000e+00] + CP 16-12 4.418e+02 [-63 4.143e-07 1.039e+00] + CP 16-13 4.426e+02 [-75 2.546e-08 9.792e-01] + CP 16-14 4.434e+02 [-85 3.153e-09 1.115e+00] + CP 16-15 4.445e+02 [-94 3.778e-10 1.265e+00] + CP 16-16 4.498e+02 [-99 1.077e-10 1.767e+00] + CP 16-17 4.550e+02 [-106 2.470e-11 1.456e+00] + CP 17-1 4.561e+02 [-6 2.113e-01 1.000e+00] + CP 17-2 4.571e+02 [-13 4.203e-02 1.205e+00] + CP 17-3 4.580e+02 [-26 2.365e-03 1.231e+00] + CP 17-4 4.589e+02 [-26 2.136e-03 7.634e-01] + CP 17-5 4.598e+02 [-29 1.140e-03 1.566e+00] + CP 17-6 4.606e+02 [-33 4.787e-04 1.103e+00] + CP 17-7 4.615e+02 [-40 9.387e-05 9.240e-01] + CP 17-8 4.623e+02 [-47 1.951e-05 1.161e+00] + CP 17-9 4.631e+02 [-62 5.662e-07 1.176e+00] + CP 17-10 4.642e+02 [-60 8.894e-07 7.771e-02] + CP 17-11 4.650e+02 [-59 1.082e-06 3.476e-01] + CP 17-12 4.657e+02 [-61 7.791e-07 1.239e+00] + CP 17-13 4.666e+02 [-67 1.656e-07 1.022e+00] + CP 17-14 4.674e+02 [-80 9.410e-09 9.674e-01] + CP 17-15 4.683e+02 [-87 1.958e-09 1.219e+00] + CP 17-16 4.736e+02 [-103 4.803e-11 1.244e+00] + CP 18-1 4.747e+02 [-9 1.205e-01 1.000e+00] + CP 18-2 4.756e+02 [-16 2.235e-02 1.201e+00] + CP 18-3 4.765e+02 [-29 1.081e-03 1.220e+00] + CP 18-4 4.773e+02 [-29 1.038e-03 6.605e-01] + CP 18-5 4.781e+02 [-31 6.379e-04 1.625e+00] + CP 18-6 4.789e+02 [-34 3.266e-04 1.075e+00] + CP 18-7 4.797e+02 [-43 4.268e-05 9.517e-01] + CP 18-8 4.806e+02 [-51 7.073e-06 1.134e+00] + CP 18-9 4.815e+02 [-63 4.479e-07 1.200e+00] + CP 18-10 4.822e+02 [-65 2.811e-07 2.000e+00] + CP 18-11 4.830e+02 [-67 1.828e-07 1.212e+00] + CP 18-12 4.841e+02 [-71 6.548e-08 8.933e-01] + CP 18-13 4.850e+02 [-78 1.445e-08 1.143e+00] + CP 18-14 4.859e+02 [-95 3.091e-10 1.143e+00] + CP 18-15 4.912e+02 [-92 5.764e-10 2.173e-01] + CP 18-16 4.966e+02 [-92 6.136e-10 2.000e+00] + CP 18-17 5.019e+02 [-92 5.693e-10 1.037e+00] + CP 18-18 5.072e+02 [-104 3.325e-11 9.810e-01] + CP 19-1 5.082e+02 [-4 3.672e-01 1.000e+00] + CP 19-2 5.090e+02 [-11 6.841e-02 1.220e+00] + CP 19-3 5.099e+02 [-24 3.317e-03 1.232e+00] + CP 19-4 5.107e+02 [-24 3.637e-03 5.202e-01] + CP 19-5 5.115e+02 [-25 2.756e-03 1.926e+00] + CP 19-6 5.123e+02 [-26 2.283e-03 1.053e+00] + CP 19-7 5.133e+02 [-37 1.916e-04 9.722e-01] + CP 19-8 5.142e+02 [-45 2.521e-05 1.114e+00] + CP 19-9 5.150e+02 [-55 2.680e-06 1.241e+00] + CP 19-10 5.161e+02 [-60 8.813e-07 1.835e+00] + CP 19-11 5.168e+02 [-64 3.165e-07 1.352e+00] + CP 19-12 5.177e+02 [-66 2.275e-07 7.870e-01] + CP 19-13 5.186e+02 [-70 8.142e-08 1.222e+00] + CP 19-14 5.196e+02 [-80 8.874e-09 1.103e+00] + CP 19-15 5.205e+02 [-84 3.526e-09 7.921e-01] + CP 19-16 5.218e+02 [-88 1.482e-09 1.362e+00] + CP 19-17 5.271e+02 [-94 3.240e-10 1.118e+00] + CP 19-18 5.323e+02 [-100 9.771e-11 8.594e-01] + CP 20-1 5.333e+02 [-14 3.850e-02 1.000e+00] + CP 20-2 5.342e+02 [-24 3.472e-03 1.063e+00] + CP 20-3 5.350e+02 [-32 5.851e-04 1.213e+00] + CP 20-4 5.358e+02 [-40 8.352e-05 1.401e+00] + CP 20-5 5.367e+02 [-46 2.407e-05 1.891e+00] + CP 20-6 5.377e+02 [-51 6.869e-06 1.498e+00] + CP 20-7 5.386e+02 [-51 7.335e-06 7.231e-01] + CP 20-8 5.398e+02 [-54 3.263e-06 1.277e+00] + CP 20-9 5.406e+02 [-62 5.549e-07 1.093e+00] + CP 20-10 5.413e+02 [-68 1.533e-07 8.606e-01] + CP 20-11 5.422e+02 [-72 5.134e-08 1.288e+00] + CP 20-12 5.430e+02 [-82 6.209e-09 1.150e+00] + CP 20-13 5.439e+02 [-84 3.404e-09 7.438e-01] + CP 20-14 5.449e+02 [-87 1.588e-09 1.392e+00] + CP 20-15 5.502e+02 [-93 4.169e-10 1.102e+00] + CP 20-16 5.555e+02 [-99 1.058e-10 8.799e-01] + CP 20-17 5.607e+02 [-105 2.867e-11 1.202e+00] + CP 21-1 5.618e+02 [-6 2.249e-01 1.000e+00] + CP 21-2 5.627e+02 [-13 4.897e-02 1.237e+00] + CP 21-3 5.635e+02 [-26 2.221e-03 1.233e+00] + CP 21-4 5.645e+02 [-25 2.900e-03 4.381e-01] + CP 21-5 5.652e+02 [-26 2.393e-03 2.000e+00] + CP 21-6 5.661e+02 [-26 2.192e-03 1.044e+00] + CP 21-7 5.670e+02 [-38 1.494e-04 9.779e-01] + CP 21-8 5.678e+02 [-47 1.831e-05 1.109e+00] + CP 21-9 5.686e+02 [-56 2.185e-06 1.255e+00] + CP 21-10 5.694e+02 [-62 5.943e-07 1.718e+00] + CP 21-11 5.702e+02 [-69 1.122e-07 1.451e+00] + CP 21-12 5.710e+02 [-68 1.373e-07 6.511e-01] + CP 21-13 5.718e+02 [-71 7.267e-08 1.374e+00] + CP 21-14 5.727e+02 [-76 2.019e-08 1.075e+00] + CP 21-15 5.735e+02 [-84 3.484e-09 9.179e-01] + CP 21-16 5.745e+02 [-90 8.484e-10 1.211e+00] + CP 21-17 5.800e+02 [-106 2.257e-11 1.183e+00] + CP 22-1 5.810e+02 [-8 1.306e-01 1.000e+00] + CP 22-2 5.819e+02 [-16 2.451e-02 1.175e+00] + CP 22-3 5.829e+02 [-26 2.156e-03 1.258e+00] + CP 22-4 5.837e+02 [-29 1.122e-03 1.870e+00] + CP 22-5 5.845e+02 [-32 5.573e-04 1.252e+00] + CP 22-6 5.853e+02 [-35 2.529e-04 8.633e-01] + CP 22-7 5.862e+02 [-41 6.689e-05 1.166e+00] + CP 22-8 5.869e+02 [-55 2.740e-06 1.130e+00] + CP 22-9 5.880e+02 [-55 2.788e-06 5.591e-01] + CP 22-10 5.888e+02 [-57 1.943e-06 1.812e+00] + CP 22-11 5.896e+02 [-58 1.377e-06 1.061e+00] + CP 22-12 5.903e+02 [-68 1.397e-07 9.631e-01] + CP 22-13 5.912e+02 [-76 2.124e-08 1.134e+00] + CP 22-14 5.920e+02 [-87 1.751e-09 1.234e+00] + CP 22-15 5.930e+02 [-90 8.948e-10 2.000e+00] + CP 22-16 5.983e+02 [-92 5.255e-10 1.260e+00] + CP 22-17 6.036e+02 [-96 2.469e-10 8.579e-01] + CP 22-18 6.089e+02 [-102 6.215e-11 1.139e+00] + CP 23-1 6.099e+02 [-3 4.537e-01 1.000e+00] + CP 23-2 6.108e+02 [-10 9.025e-02 1.255e+00] + CP 23-3 6.116e+02 [-24 3.941e-03 1.233e+00] + CP 23-4 6.125e+02 [-22 5.548e-03 4.475e-01] + CP 23-5 6.134e+02 [-23 4.437e-03 2.000e+00] + CP 23-6 6.143e+02 [-23 4.071e-03 1.043e+00] + CP 23-7 6.152e+02 [-35 2.701e-04 9.785e-01] + CP 23-8 6.160e+02 [-44 3.287e-05 1.109e+00] + CP 23-9 6.168e+02 [-54 3.963e-06 1.256e+00] + CP 23-10 6.176e+02 [-59 1.059e-06 1.710e+00] + CP 23-11 6.184e+02 [-67 1.851e-07 1.462e+00] + CP 23-12 6.192e+02 [-66 2.415e-07 6.323e-01] + CP 23-13 6.200e+02 [-68 1.334e-07 1.402e+00] + CP 23-14 6.208e+02 [-73 4.104e-08 1.072e+00] + CP 23-15 6.216e+02 [-81 6.498e-09 9.264e-01] + CP 23-16 6.226e+02 [-88 1.490e-09 1.199e+00] + CP 23-17 6.279e+02 [-104 3.277e-11 1.194e+00] + CP 24-1 6.290e+02 [-6 2.118e-01 1.000e+00] + CP 24-2 6.299e+02 [-13 4.207e-02 1.225e+00] + CP 24-3 6.307e+02 [-26 2.088e-03 1.248e+00] + CP 24-4 6.317e+02 [-26 2.398e-03 4.426e-01] + CP 24-5 6.325e+02 [-26 2.044e-03 2.000e+00] + CP 24-6 6.333e+02 [-27 1.851e-03 1.049e+00] + CP 24-7 6.342e+02 [-38 1.426e-04 9.747e-01] + CP 24-8 6.350e+02 [-47 1.820e-05 1.113e+00] + CP 24-9 6.359e+02 [-56 2.043e-06 1.250e+00] + CP 24-10 6.367e+02 [-62 6.303e-07 1.801e+00] + CP 24-11 6.374e+02 [-67 1.877e-07 1.389e+00] + CP 24-12 6.382e+02 [-67 1.616e-07 7.469e-01] + CP 24-13 6.390e+02 [-71 6.613e-08 1.261e+00] + CP 24-14 6.398e+02 [-79 1.012e-08 1.094e+00] + CP 24-15 6.406e+02 [-85 2.974e-09 8.469e-01] + CP 24-16 6.416e+02 [-89 1.035e-09 1.297e+00] + CP 24-17 6.470e+02 [-98 1.388e-10 1.145e+00] + CP 24-18 6.523e+02 [-101 6.629e-11 7.763e-01] + CP 25-1 6.534e+02 [-7 1.677e-01 1.000e+00] + CP 25-2 6.544e+02 [-14 3.894e-02 1.270e+00] + CP 25-3 6.553e+02 [-27 1.904e-03 1.232e+00] + CP 25-4 6.562e+02 [-25 2.626e-03 4.876e-01] + CP 25-5 6.570e+02 [-27 1.966e-03 1.815e+00] + CP 25-6 6.578e+02 [-28 1.439e-03 1.048e+00] + CP 25-7 6.587e+02 [-39 1.121e-04 9.729e-01] + CP 25-8 6.595e+02 [-48 1.514e-05 1.119e+00] + CP 25-9 6.605e+02 [-58 1.566e-06 1.243e+00] + CP 25-10 6.616e+02 [-62 5.437e-07 1.887e+00] + CP 25-11 6.623e+02 [-66 2.286e-07 1.329e+00] + CP 25-12 6.633e+02 [-68 1.453e-07 8.133e-01] + CP 25-13 6.641e+02 [-73 4.693e-08 1.198e+00] + CP 25-14 6.649e+02 [-84 3.809e-09 1.110e+00] + CP 25-15 6.658e+02 [-87 1.979e-09 7.341e-01] + CP 25-16 6.668e+02 [-90 9.721e-10 1.438e+00] + CP 25-17 6.721e+02 [-95 2.983e-10 1.100e+00] + CP 25-18 6.774e+02 [-101 6.718e-11 8.993e-01] + CP 26-1 6.786e+02 [-5 2.662e-01 1.000e+00] + CP 26-2 6.796e+02 [-12 6.174e-02 1.252e+00] + CP 26-3 6.804e+02 [-25 2.610e-03 1.244e+00] + CP 26-4 6.814e+02 [-23 4.052e-03 3.681e-01] + CP 26-5 6.822e+02 [-24 3.633e-03 2.000e+00] + CP 26-6 6.830e+02 [-24 3.345e-03 1.041e+00] + CP 26-7 6.839e+02 [-36 2.157e-04 9.788e-01] + CP 26-8 6.847e+02 [-45 2.653e-05 1.111e+00] + CP 26-9 6.856e+02 [-54 3.193e-06 1.260e+00] + CP 26-10 6.865e+02 [-60 8.787e-07 1.735e+00] + CP 26-11 6.873e+02 [-67 1.730e-07 1.453e+00] + CP 26-12 6.881e+02 [-66 2.098e-07 6.560e-01] + CP 26-13 6.889e+02 [-69 1.097e-07 1.368e+00] + CP 26-14 6.897e+02 [-75 2.976e-08 1.076e+00] + CP 26-15 6.906e+02 [-82 5.275e-09 9.150e-01] + CP 26-16 6.916e+02 [-88 1.311e-09 1.215e+00] + CP 26-17 6.969e+02 [-103 4.027e-11 1.182e+00] + CP 27-1 6.981e+02 [-5 2.785e-01 1.000e+00] + CP 27-2 6.989e+02 [-12 5.957e-02 1.267e+00] + CP 27-3 6.998e+02 [-25 2.658e-03 1.232e+00] + CP 27-4 7.007e+02 [-24 3.851e-03 4.665e-01] + CP 27-5 7.016e+02 [-25 2.956e-03 1.878e+00] + CP 27-6 7.024e+02 [-26 2.357e-03 1.045e+00] + CP 27-7 7.033e+02 [-37 1.663e-04 9.762e-01] + CP 27-8 7.041e+02 [-46 2.114e-05 1.113e+00] + CP 27-9 7.050e+02 [-56 2.363e-06 1.248e+00] + CP 27-10 7.061e+02 [-61 7.093e-07 1.775e+00] + CP 27-11 7.069e+02 [-67 1.973e-07 1.391e+00] + CP 27-12 7.077e+02 [-67 1.737e-07 7.402e-01] + CP 27-13 7.089e+02 [-71 7.242e-08 1.264e+00] + CP 27-14 7.097e+02 [-79 1.152e-08 1.091e+00] + CP 27-15 7.106e+02 [-84 3.184e-09 8.568e-01] + CP 27-16 7.159e+02 [-89 1.063e-09 1.283e+00] + CP 27-17 7.212e+02 [-98 1.294e-10 1.145e+00] + CP 27-18 7.265e+02 [-101 6.727e-11 7.548e-01] + CP 28-1 7.277e+02 [-2 5.409e-01 1.000e+00] + CP 28-2 7.286e+02 [-9 1.108e-01 1.273e+00] + CP 28-3 7.295e+02 [-23 4.846e-03 1.242e+00] + CP 28-4 7.304e+02 [-21 7.497e-03 4.361e-01] + CP 28-5 7.311e+02 [-22 6.066e-03 2.000e+00] + CP 28-6 7.319e+02 [-22 5.563e-03 1.043e+00] + CP 28-7 7.327e+02 [-34 3.717e-04 9.781e-01] + CP 28-8 7.335e+02 [-43 4.588e-05 1.111e+00] + CP 28-9 7.345e+02 [-52 5.470e-06 1.258e+00] + CP 28-10 7.354e+02 [-58 1.528e-06 1.742e+00] + CP 28-11 7.362e+02 [-64 3.195e-07 1.444e+00] + CP 28-12 7.369e+02 [-64 3.681e-07 6.704e-01] + CP 28-13 7.377e+02 [-67 1.860e-07 1.348e+00] + CP 28-14 7.386e+02 [-73 4.650e-08 1.079e+00] + CP 28-15 7.394e+02 [-80 8.845e-09 9.074e-01] + CP 28-16 7.403e+02 [-86 2.302e-09 1.224e+00] + CP 28-17 7.456e+02 [-100 9.819e-11 1.178e+00] + CP 29-1 7.468e+02 [-11 6.480e-02 1.000e+00] + CP 29-2 7.476e+02 [-19 1.080e-02 1.140e+00] + CP 29-3 7.484e+02 [-29 1.026e-03 1.239e+00] + CP 29-4 7.492e+02 [-34 3.474e-04 1.739e+00] + CP 29-5 7.500e+02 [-39 1.229e-04 1.290e+00] + CP 29-6 7.508e+02 [-41 6.778e-05 8.342e-01] + CP 29-7 7.516e+02 [-47 1.966e-05 1.166e+00] + CP 29-8 7.524e+02 [-59 1.246e-06 1.102e+00] + CP 29-9 7.535e+02 [-61 6.644e-07 7.347e-01] + CP 29-10 7.543e+02 [-64 3.212e-07 1.408e+00] + CP 29-11 7.553e+02 [-70 9.347e-08 1.091e+00] + CP 29-12 7.562e+02 [-77 1.833e-08 9.133e-01] + CP 29-13 7.570e+02 [-83 4.219e-09 1.183e+00] + CP 29-14 7.581e+02 [-99 1.072e-10 1.163e+00] + CP 29-15 7.634e+02 [-97 1.939e-10 3.780e-01] + CP 29-16 7.688e+02 [-97 1.697e-10 2.000e+00] + CP 29-17 7.742e+02 [-98 1.550e-10 1.045e+00] + CP 29-18 7.795e+02 [-108 1.415e-11 9.645e-01] + CP 30-1 7.806e+02 [-5 3.000e-01 1.000e+00] + CP 30-2 7.815e+02 [-11 7.543e-02 1.289e+00] + CP 30-3 7.824e+02 [-23 4.004e-03 1.240e+00] + CP 30-4 7.834e+02 [-22 5.689e-03 4.928e-01] + CP 30-5 7.842e+02 [-23 4.226e-03 1.819e+00] + CP 30-6 7.850e+02 [-25 3.100e-03 1.049e+00] + CP 30-7 7.859e+02 [-36 2.490e-04 9.717e-01] + CP 30-8 7.867e+02 [-44 3.435e-05 1.123e+00] + CP 30-9 7.876e+02 [-54 3.449e-06 1.244e+00] + CP 30-10 7.884e+02 [-58 1.283e-06 1.961e+00] + CP 30-11 7.894e+02 [-61 6.447e-07 1.306e+00] + CP 30-12 7.901e+02 [-64 3.613e-07 8.373e-01] + CP 30-13 7.909e+02 [-69 1.054e-07 1.180e+00] + CP 30-14 7.917e+02 [-82 5.984e-09 1.119e+00] + CP 30-15 7.927e+02 [-83 4.419e-09 6.461e-01] + CP 30-16 7.936e+02 [-85 2.628e-09 1.587e+00] + CP 30-17 7.990e+02 [-89 1.238e-09 1.079e+00] + CP 30-18 8.044e+02 [-97 1.827e-10 9.403e-01] + CP 30-19 8.098e+02 [-104 3.528e-11 1.165e+00] + CP 31-1 8.108e+02 [-7 1.670e-01 1.000e+00] + CP 31-2 8.116e+02 [-15 3.079e-02 1.154e+00] + CP 31-3 8.126e+02 [-24 3.680e-03 1.286e+00] + CP 31-4 8.134e+02 [-28 1.263e-03 1.818e+00] + CP 31-5 8.142e+02 [-33 4.321e-04 1.363e+00] + CP 31-6 8.151e+02 [-34 3.227e-04 7.820e-01] + CP 31-7 8.159e+02 [-39 1.192e-04 1.231e+00] + CP 31-8 8.167e+02 [-48 1.382e-05 1.104e+00] + CP 31-9 8.175e+02 [-52 5.342e-06 8.004e-01] + CP 31-10 8.183e+02 [-56 2.191e-06 1.352e+00] + CP 31-11 8.194e+02 [-63 4.523e-07 1.121e+00] + CP 31-12 8.205e+02 [-68 1.457e-07 8.487e-01] + CP 31-13 8.212e+02 [-73 4.812e-08 1.265e+00] + CP 31-14 8.221e+02 [-82 5.333e-09 1.140e+00] + CP 31-15 8.230e+02 [-85 2.872e-09 7.416e-01] + CP 31-16 8.283e+02 [-88 1.358e-09 1.405e+00] + CP 31-17 8.337e+02 [-94 3.773e-10 1.099e+00] + CP 31-18 8.390e+02 [-100 9.008e-11 8.879e-01] + CP 32-1 8.400e+02 [0 9.961e-01 1.000e+00] + CP 32-2 8.410e+02 [-6 2.020e-01 1.302e+00] + CP 32-3 8.419e+02 [-19 1.147e-02 1.240e+00] + CP 32-4 8.427e+02 [-18 1.479e-02 5.281e-01] + CP 32-5 8.435e+02 [-19 1.031e-02 1.699e+00] + CP 32-6 8.443e+02 [-22 6.300e-03 1.054e+00] + CP 32-7 8.451e+02 [-32 5.853e-04 9.655e-01] + CP 32-8 8.459e+02 [-40 8.865e-05 1.133e+00] + CP 32-9 8.467e+02 [-51 7.446e-06 1.232e+00] + CP 32-10 8.476e+02 [-54 3.603e-06 2.000e+00] + CP 32-11 8.483e+02 [-56 2.126e-06 1.258e+00] + CP 32-12 8.492e+02 [-60 9.600e-07 8.672e-01] + CP 32-13 8.500e+02 [-66 2.444e-07 1.162e+00] + CP 32-14 8.508e+02 [-81 7.424e-09 1.132e+00] + CP 32-15 8.517e+02 [-79 1.021e-08 4.673e-01] + CP 32-16 8.525e+02 [-80 8.110e-09 2.000e+00] + CP 32-17 8.578e+02 [-81 7.347e-09 1.049e+00] + CP 32-18 8.631e+02 [-92 5.784e-10 9.729e-01] + CP 32-19 8.685e+02 [-100 8.611e-11 1.132e+00] + CP 33-1 8.695e+02 [-6 2.510e-01 1.000e+00] + CP 33-2 8.704e+02 [-12 5.396e-02 1.255e+00] + CP 33-3 8.712e+02 [-26 2.013e-03 1.260e+00] + CP 33-4 8.721e+02 [-24 3.600e-03 2.186e-01] + CP 33-5 8.730e+02 [-24 3.820e-03 1.966e+00] + CP 33-6 8.740e+02 [-24 3.409e-03 1.039e+00] + CP 33-7 8.748e+02 [-36 2.078e-04 9.794e-01] + CP 33-8 8.757e+02 [-45 2.591e-05 1.116e+00] + CP 33-9 8.765e+02 [-55 3.119e-06 1.266e+00] + CP 33-10 8.773e+02 [-60 8.895e-07 1.768e+00] + CP 33-11 8.781e+02 [-66 2.014e-07 1.444e+00] + CP 33-12 8.788e+02 [-66 2.248e-07 6.820e-01] + CP 33-13 8.796e+02 [-69 1.103e-07 1.334e+00] + CP 33-14 8.804e+02 [-75 2.583e-08 1.081e+00] + CP 33-15 8.813e+02 [-82 5.248e-09 8.995e-01] + CP 33-16 8.821e+02 [-88 1.430e-09 1.235e+00] + CP 33-17 8.875e+02 [-101 7.884e-11 1.172e+00] + CP 34-1 8.886e+02 [-6 2.494e-01 1.000e+00] + CP 34-2 8.895e+02 [-11 6.429e-02 1.309e+00] + CP 34-3 8.904e+02 [-23 4.187e-03 1.239e+00] + CP 34-4 8.914e+02 [-22 5.150e-03 5.506e-01] + CP 34-5 8.922e+02 [-24 3.460e-03 1.632e+00] + CP 34-6 8.930e+02 [-27 1.880e-03 1.058e+00] + CP 34-7 8.940e+02 [-37 1.906e-04 9.610e-01] + CP 34-8 8.949e+02 [-45 3.062e-05 1.140e+00] + CP 34-9 8.957e+02 [-56 2.233e-06 1.224e+00] + CP 34-10 8.966e+02 [-58 1.300e-06 2.000e+00] + CP 34-11 8.974e+02 [-60 8.030e-07 1.236e+00] + CP 34-12 8.982e+02 [-64 3.278e-07 8.784e-01] + CP 34-13 8.993e+02 [-71 7.902e-08 1.156e+00] + CP 34-14 9.000e+02 [-87 1.766e-09 1.138e+00] + CP 34-15 9.009e+02 [-84 3.296e-09 3.607e-01] + CP 34-16 9.017e+02 [-85 2.998e-09 2.000e+00] + CP 34-17 9.071e+02 [-85 2.741e-09 1.045e+00] + CP 34-18 9.124e+02 [-97 1.923e-10 9.763e-01] + CP 34-19 9.177e+02 [-106 2.445e-11 1.118e+00] + CP 35-1 9.189e+02 [-4 3.796e-01 1.000e+00] + CP 35-2 9.198e+02 [-10 9.919e-02 1.291e+00] + CP 35-3 9.207e+02 [-23 4.584e-03 1.251e+00] + CP 35-4 9.215e+02 [-21 7.814e-03 4.295e-01] + CP 35-5 9.223e+02 [-21 6.384e-03 2.000e+00] + CP 35-6 9.232e+02 [-22 5.860e-03 1.043e+00] + CP 35-7 9.240e+02 [-34 3.952e-04 9.775e-01] + CP 35-8 9.249e+02 [-43 4.956e-05 1.114e+00] + CP 35-9 9.257e+02 [-52 5.794e-06 1.259e+00] + CP 35-10 9.265e+02 [-57 1.707e-06 1.781e+00] + CP 35-11 9.275e+02 [-63 4.371e-07 1.420e+00] + CP 35-12 9.283e+02 [-63 4.347e-07 7.119e-01] + CP 35-13 9.291e+02 [-67 1.970e-07 1.298e+00] + CP 35-14 9.299e+02 [-74 3.838e-08 1.087e+00] + CP 35-15 9.310e+02 [-80 9.117e-09 8.791e-01] + CP 35-16 9.319e+02 [-85 2.768e-09 1.260e+00] + CP 35-17 9.330e+02 [-96 2.475e-10 1.157e+00] + CP 35-18 9.384e+02 [-97 1.755e-10 6.778e-01] + CP 35-19 9.437e+02 [-100 9.714e-11 1.508e+00] + CP 36-1 9.448e+02 [0 8.453e-01 1.000e+00] + CP 36-2 9.457e+02 [-7 1.885e-01 1.304e+00] + CP 36-3 9.466e+02 [-19 1.100e-02 1.239e+00] + CP 36-4 9.474e+02 [-18 1.408e-02 5.335e-01] + CP 36-5 9.482e+02 [-20 9.693e-03 1.672e+00] + CP 36-6 9.491e+02 [-22 5.672e-03 1.055e+00] + CP 36-7 9.499e+02 [-32 5.327e-04 9.647e-01] + CP 36-8 9.507e+02 [-40 8.137e-05 1.134e+00] + CP 36-9 9.517e+02 [-51 6.605e-06 1.229e+00] + CP 36-10 9.525e+02 [-54 3.304e-06 2.000e+00] + CP 36-11 9.534e+02 [-57 1.979e-06 1.251e+00] + CP 36-12 9.542e+02 [-60 8.640e-07 8.713e-01] + CP 36-13 9.550e+02 [-66 2.150e-07 1.158e+00] + CP 36-14 9.557e+02 [-82 5.878e-09 1.133e+00] + CP 36-15 9.566e+02 [-80 8.879e-09 4.350e-01] + CP 36-16 9.575e+02 [-81 7.367e-09 2.000e+00] + CP 36-17 9.628e+02 [-81 6.697e-09 1.048e+00] + CP 36-18 9.681e+02 [-92 5.150e-10 9.731e-01] + CP 36-19 9.735e+02 [-101 6.538e-11 1.108e+00] + CP 37-1 9.747e+02 [0 9.684e-01 1.000e+00] + CP 37-2 9.758e+02 [-7 1.974e-01 1.311e+00] + CP 37-3 9.768e+02 [-19 1.096e-02 1.248e+00] + CP 37-4 9.778e+02 [-18 1.519e-02 5.088e-01] + CP 37-5 9.787e+02 [-19 1.095e-02 1.782e+00] + CP 37-6 9.795e+02 [-21 7.587e-03 1.052e+00] + CP 37-7 9.803e+02 [-31 6.559e-04 9.688e-01] + CP 37-8 9.812e+02 [-40 9.456e-05 1.129e+00] + CP 37-9 9.820e+02 [-50 8.796e-06 1.241e+00] + CP 37-10 9.828e+02 [-54 3.740e-06 2.000e+00] + CP 37-11 9.837e+02 [-56 2.101e-06 1.281e+00] + CP 37-12 9.845e+02 [-59 1.050e-06 8.545e-01] + CP 37-13 9.853e+02 [-65 2.840e-07 1.170e+00] + CP 37-14 9.861e+02 [-79 1.152e-08 1.127e+00] + CP 37-15 9.869e+02 [-79 1.199e-08 5.514e-01] + CP 37-16 9.878e+02 [-80 8.413e-09 1.837e+00] + CP 37-17 9.888e+02 [-82 6.157e-09 1.060e+00] + CP 37-18 9.942e+02 [-92 6.226e-10 9.632e-01] + CP 37-19 9.995e+02 [-100 9.591e-11 1.138e+00] + CP 38-1 1.001e+03 [-9 1.207e-01 1.000e+00] + CP 38-2 1.001e+03 [-15 2.706e-02 1.238e+00] + CP 38-3 1.002e+03 [-30 9.345e-04 1.244e+00] + CP 38-4 1.003e+03 [-27 1.614e-03 2.129e-01] + CP 38-5 1.004e+03 [-27 1.698e-03 1.893e+00] + CP 38-6 1.005e+03 [-28 1.405e-03 1.036e+00] + CP 38-7 1.006e+03 [-41 7.928e-05 9.812e-01] + CP 38-8 1.007e+03 [-50 9.371e-06 1.108e+00] + CP 38-9 1.007e+03 [-59 1.134e-06 1.257e+00] + CP 38-10 1.008e+03 [-65 2.885e-07 1.683e+00] + CP 38-11 1.009e+03 [-73 4.172e-08 1.478e+00] + CP 38-12 1.010e+03 [-72 6.176e-08 5.922e-01] + CP 38-13 1.011e+03 [-74 3.697e-08 1.460e+00] + CP 38-14 1.011e+03 [-78 1.373e-08 1.065e+00] + CP 38-15 1.012e+03 [-87 1.788e-09 9.426e-01] + CP 38-16 1.018e+03 [-94 3.582e-10 1.175e+00] + CP 38-17 1.023e+03 [-109 1.092e-11 1.205e+00] + CP 39-1 1.024e+03 [-3 4.267e-01 1.000e+00] + CP 39-2 1.025e+03 [-9 1.187e-01 1.321e+00] + CP 39-3 1.026e+03 [-20 8.202e-03 1.245e+00] + CP 39-4 1.027e+03 [-19 1.032e-02 5.488e-01] + CP 39-5 1.027e+03 [-21 6.981e-03 1.653e+00] + CP 39-6 1.028e+03 [-24 3.929e-03 1.058e+00] + CP 39-7 1.029e+03 [-33 3.994e-04 9.608e-01] + CP 39-8 1.030e+03 [-41 6.423e-05 1.141e+00] + CP 39-9 1.031e+03 [-53 4.669e-06 1.227e+00] + CP 39-10 1.032e+03 [-55 2.772e-06 2.000e+00] + CP 39-11 1.032e+03 [-57 1.706e-06 1.238e+00] + CP 39-12 1.033e+03 [-61 7.049e-07 8.765e-01] + CP 39-13 1.034e+03 [-67 1.720e-07 1.158e+00] + CP 39-14 1.035e+03 [-84 3.950e-09 1.138e+00] + CP 39-15 1.036e+03 [-81 7.274e-09 3.750e-01] + CP 39-16 1.037e+03 [-81 6.505e-09 2.000e+00] + CP 39-17 1.038e+03 [-82 5.945e-09 1.045e+00] + CP 39-18 1.043e+03 [-93 4.279e-10 9.750e-01] + CP 39-19 1.048e+03 [-102 5.685e-11 1.123e+00] + CP 40-1 1.049e+03 [-5 2.787e-01 1.000e+00] + CP 40-2 1.050e+03 [-13 4.389e-02 1.112e+00] + CP 40-3 1.051e+03 [-21 7.186e-03 1.297e+00] + CP 40-4 1.052e+03 [-28 1.277e-03 1.549e+00] + CP 40-5 1.053e+03 [-34 3.168e-04 1.821e+00] + CP 40-6 1.054e+03 [-38 1.354e-04 1.476e+00] + CP 40-7 1.055e+03 [-40 8.244e-05 9.798e-01] + CP 40-8 1.056e+03 [-46 2.362e-05 1.160e+00] + CP 40-9 1.057e+03 [-55 2.679e-06 1.307e+00] + CP 40-10 1.058e+03 [-59 1.036e-06 2.000e+00] + CP 40-11 1.059e+03 [-62 5.048e-07 1.345e+00] + CP 40-12 1.059e+03 [-64 3.265e-07 8.169e-01] + CP 40-13 1.061e+03 [-69 1.039e-07 1.197e+00] + CP 40-14 1.061e+03 [-81 7.799e-09 1.114e+00] + CP 40-15 1.062e+03 [-83 4.557e-09 7.040e-01] + CP 40-16 1.064e+03 [-86 2.411e-09 1.493e+00] + CP 40-17 1.069e+03 [-90 8.823e-10 1.093e+00] + CP 40-18 1.074e+03 [-97 1.709e-10 9.160e-01] + CP 40-19 1.080e+03 [-103 4.015e-11 1.197e+00] + CP 41-1 1.081e+03 [3 2.234e+00 1.000e+00] + CP 41-2 1.082e+03 [-2 5.608e-01 1.329e+00] + CP 41-3 1.083e+03 [-14 3.878e-02 1.245e+00] + CP 41-4 1.084e+03 [-13 4.519e-02 5.673e-01] + CP 41-5 1.085e+03 [-15 2.936e-02 1.606e+00] + CP 41-6 1.085e+03 [-18 1.506e-02 1.061e+00] + CP 41-7 1.086e+03 [-27 1.655e-03 9.566e-01] + CP 41-8 1.087e+03 [-35 2.799e-04 1.148e+00] + CP 41-9 1.088e+03 [-47 1.772e-05 1.221e+00] + CP 41-10 1.089e+03 [-49 1.255e-05 2.000e+00] + CP 41-11 1.090e+03 [-50 7.969e-06 1.223e+00] + CP 41-12 1.090e+03 [-55 3.064e-06 8.841e-01] + CP 41-13 1.091e+03 [-61 7.212e-07 1.155e+00] + CP 41-14 1.092e+03 [-78 1.293e-08 1.143e+00] + CP 41-15 1.093e+03 [-75 3.064e-08 2.793e-01] + CP 41-16 1.094e+03 [-75 3.054e-08 1.502e+00] + CP 41-17 1.095e+03 [-78 1.290e-08 1.056e+00] + CP 41-18 1.100e+03 [-88 1.403e-09 9.525e-01] + CP 41-19 1.106e+03 [-95 2.728e-10 1.177e+00] + CP 41-20 1.111e+03 [-102 5.156e-11 1.088e+00] + CP 42-1 1.112e+03 [-5 2.877e-01 1.000e+00] + CP 42-2 1.113e+03 [-11 6.441e-02 1.270e+00] + CP 42-3 1.114e+03 [-27 1.983e-03 1.269e+00] + CP 42-4 1.115e+03 [-23 4.676e-03 1.709e-01] + CP 42-5 1.116e+03 [-22 5.177e-03 1.366e+00] + CP 42-6 1.116e+03 [-28 1.527e-03 1.055e+00] + CP 42-7 1.117e+03 [-37 1.859e-04 9.410e-01] + CP 42-8 1.118e+03 [-43 4.013e-05 1.197e+00] + CP 42-9 1.119e+03 [-61 6.914e-07 1.208e+00] + CP 42-10 1.120e+03 [-56 2.267e-06 1.196e-01] + CP 42-11 1.121e+03 [-55 2.662e-06 6.751e-01] + CP 42-12 1.122e+03 [-59 1.055e-06 1.118e+00] + CP 42-13 1.123e+03 [-71 7.012e-08 1.049e+00] + CP 42-14 1.123e+03 [-77 1.906e-08 8.260e-01] + CP 42-15 1.124e+03 [-81 7.809e-09 1.398e+00] + CP 42-16 1.125e+03 [-87 1.816e-09 1.134e+00] + CP 42-17 1.126e+03 [-92 6.067e-10 8.504e-01] + CP 42-18 1.132e+03 [-96 2.001e-10 1.265e+00] + CP 42-19 1.137e+03 [-106 2.235e-11 1.138e+00] + CP 43-1 1.138e+03 [-4 3.590e-01 1.000e+00] + CP 43-2 1.139e+03 [-9 1.000e-01 1.333e+00] + CP 43-3 1.140e+03 [-21 7.799e-03 1.243e+00] + CP 43-4 1.141e+03 [-20 8.913e-03 5.804e-01] + CP 43-5 1.142e+03 [-22 5.679e-03 1.571e+00] + CP 43-6 1.143e+03 [-25 2.717e-03 1.063e+00] + CP 43-7 1.143e+03 [-35 3.134e-04 9.535e-01] + CP 43-8 1.144e+03 [-42 5.483e-05 1.152e+00] + CP 43-9 1.145e+03 [-55 3.091e-06 1.215e+00] + CP 43-10 1.146e+03 [-56 2.503e-06 2.000e+00] + CP 43-11 1.147e+03 [-57 1.625e-06 1.213e+00] + CP 43-12 1.148e+03 [-62 5.909e-07 8.898e-01] + CP 43-13 1.148e+03 [-68 1.349e-07 1.151e+00] + CP 43-14 1.149e+03 [-86 2.155e-09 1.146e+00] + CP 43-15 1.150e+03 [-82 5.699e-09 1.956e-01] + CP 43-16 1.151e+03 [-82 6.184e-09 2.000e+00] + CP 43-17 1.152e+03 [-82 5.725e-09 1.039e+00] + CP 43-18 1.157e+03 [-94 3.488e-10 9.790e-01] + CP 43-19 1.163e+03 [-103 4.416e-11 1.120e+00] + CP 44-1 1.164e+03 [-2 6.090e-01 1.000e+00] + CP 44-2 1.165e+03 [-7 1.775e-01 1.314e+00] + CP 44-3 1.166e+03 [-20 9.898e-03 1.255e+00] + CP 44-4 1.167e+03 [-18 1.580e-02 4.748e-01] + CP 44-5 1.168e+03 [-19 1.209e-02 1.945e+00] + CP 44-6 1.169e+03 [-19 1.038e-02 1.047e+00] + CP 44-7 1.170e+03 [-31 7.711e-04 9.744e-01] + CP 44-8 1.171e+03 [-39 1.016e-04 1.120e+00] + CP 44-9 1.172e+03 [-49 1.101e-05 1.254e+00] + CP 44-10 1.172e+03 [-54 3.740e-06 1.893e+00] + CP 44-11 1.174e+03 [-58 1.499e-06 1.352e+00] + CP 44-12 1.174e+03 [-59 1.046e-06 7.963e-01] + CP 44-13 1.175e+03 [-64 3.615e-07 1.215e+00] + CP 44-14 1.176e+03 [-74 3.561e-08 1.107e+00] + CP 44-15 1.177e+03 [-78 1.583e-08 7.677e-01] + CP 44-16 1.178e+03 [-81 7.140e-09 1.396e+00] + CP 44-17 1.179e+03 [-87 1.833e-09 1.111e+00] + CP 44-18 1.185e+03 [-93 4.862e-10 8.781e-01] + CP 44-19 1.190e+03 [-98 1.409e-10 1.235e+00] + CP 44-20 1.195e+03 [-110 9.787e-12 1.156e+00] + CP 45-1 1.197e+03 [0 1.068e+00 1.000e+00] + CP 45-2 1.198e+03 [-4 3.412e-01 1.329e+00] + CP 45-3 1.199e+03 [-15 2.638e-02 1.243e+00] + CP 45-4 1.200e+03 [-15 3.142e-02 5.699e-01] + CP 45-5 1.200e+03 [-16 2.044e-02 1.593e+00] + CP 45-6 1.201e+03 [-19 1.026e-02 1.061e+00] + CP 45-7 1.202e+03 [-29 1.131e-03 9.562e-01] + CP 45-8 1.203e+03 [-37 1.918e-04 1.148e+00] + CP 45-9 1.204e+03 [-49 1.187e-05 1.219e+00] + CP 45-10 1.205e+03 [-50 8.557e-06 2.000e+00] + CP 45-11 1.206e+03 [-52 5.471e-06 1.220e+00] + CP 45-12 1.207e+03 [-56 2.066e-06 8.861e-01] + CP 45-13 1.208e+03 [-63 4.799e-07 1.153e+00] + CP 45-14 1.209e+03 [-80 8.366e-09 1.143e+00] + CP 45-15 1.210e+03 [-76 2.019e-08 2.558e-01] + CP 45-16 1.211e+03 [-76 2.062e-08 2.000e+00] + CP 45-17 1.212e+03 [-77 1.902e-08 1.040e+00] + CP 45-18 1.217e+03 [-89 1.218e-09 9.779e-01] + CP 45-19 1.222e+03 [-98 1.562e-10 1.121e+00] + CP 45-20 1.228e+03 [-107 1.840e-11 1.269e+00] + CP 46-1 1.229e+03 [2 1.948e+00 1.000e+00] + CP 46-2 1.230e+03 [-4 3.622e-01 1.333e+00] + CP 46-3 1.231e+03 [-16 2.312e-02 1.252e+00] + CP 46-4 1.232e+03 [-15 2.921e-02 5.431e-01] + CP 46-5 1.233e+03 [-17 1.984e-02 1.686e+00] + CP 46-6 1.234e+03 [-19 1.177e-02 1.058e+00] + CP 46-7 1.234e+03 [-29 1.182e-03 9.618e-01] + CP 46-8 1.235e+03 [-37 1.877e-04 1.141e+00] + CP 46-9 1.236e+03 [-48 1.419e-05 1.231e+00] + CP 46-10 1.237e+03 [-50 8.144e-06 2.000e+00] + CP 46-11 1.238e+03 [-53 4.928e-06 1.246e+00] + CP 46-12 1.239e+03 [-56 2.119e-06 8.718e-01] + CP 46-13 1.239e+03 [-62 5.313e-07 1.162e+00] + CP 46-14 1.240e+03 [-78 1.371e-08 1.137e+00] + CP 46-15 1.241e+03 [-76 2.281e-08 4.147e-01] + CP 46-16 1.242e+03 [-77 1.943e-08 2.000e+00] + CP 46-17 1.243e+03 [-77 1.767e-08 1.048e+00] + CP 46-18 1.248e+03 [-88 1.334e-09 9.741e-01] + CP 46-19 1.254e+03 [-97 1.781e-10 1.123e+00] + CP 46-20 1.259e+03 [-104 3.201e-11 1.118e+00] + CP 47-1 1.260e+03 [-6 2.019e-01 1.000e+00] + CP 47-2 1.261e+03 [-12 5.144e-02 1.296e+00] + CP 47-3 1.262e+03 [-25 2.538e-03 1.245e+00] + CP 47-4 1.263e+03 [-24 3.968e-03 4.705e-01] + CP 47-5 1.264e+03 [-25 3.021e-03 1.895e+00] + CP 47-6 1.265e+03 [-26 2.456e-03 1.045e+00] + CP 47-7 1.266e+03 [-37 1.758e-04 9.758e-01] + CP 47-8 1.266e+03 [-46 2.239e-05 1.114e+00] + CP 47-9 1.267e+03 [-56 2.470e-06 1.249e+00] + CP 47-10 1.268e+03 [-61 7.687e-07 1.807e+00] + CP 47-11 1.269e+03 [-66 2.403e-07 1.378e+00] + CP 47-12 1.270e+03 [-67 1.956e-07 7.601e-01] + CP 47-13 1.270e+03 [-71 7.650e-08 1.245e+00] + CP 47-14 1.271e+03 [-79 1.044e-08 1.096e+00] + CP 47-15 1.272e+03 [-84 3.326e-09 8.331e-01] + CP 47-16 1.273e+03 [-89 1.217e-09 1.311e+00] + CP 47-17 1.278e+03 [-97 1.912e-10 1.133e+00] + CP 47-18 1.284e+03 [-100 8.029e-11 8.002e-01] + CP 48-1 1.285e+03 [-2 6.016e-01 1.000e+00] + CP 48-2 1.286e+03 [-7 1.833e-01 1.340e+00] + CP 48-3 1.287e+03 [-18 1.481e-02 1.248e+00] + CP 48-4 1.288e+03 [-17 1.755e-02 5.736e-01] + CP 48-5 1.289e+03 [-19 1.137e-02 1.598e+00] + CP 48-6 1.290e+03 [-22 5.746e-03 1.062e+00] + CP 48-7 1.291e+03 [-31 6.501e-04 9.547e-01] + CP 48-8 1.292e+03 [-39 1.123e-04 1.151e+00] + CP 48-9 1.293e+03 [-51 6.579e-06 1.219e+00] + CP 48-10 1.294e+03 [-52 5.149e-06 2.000e+00] + CP 48-11 1.295e+03 [-54 3.309e-06 1.218e+00] + CP 48-12 1.295e+03 [-59 1.238e-06 8.865e-01] + CP 48-13 1.296e+03 [-65 2.882e-07 1.154e+00] + CP 48-14 1.297e+03 [-83 4.664e-09 1.144e+00] + CP 48-15 1.298e+03 [-79 1.230e-08 2.422e-01] + CP 48-16 1.299e+03 [-78 1.274e-08 2.000e+00] + CP 48-17 1.300e+03 [-79 1.176e-08 1.040e+00] + CP 48-18 1.301e+03 [-91 7.486e-10 9.779e-01] + CP 48-19 1.307e+03 [-100 9.650e-11 1.122e+00] + CP 49-1 1.308e+03 [-3 4.239e-01 1.000e+00] + CP 49-2 1.309e+03 [-11 7.152e-02 1.129e+00] + CP 49-3 1.310e+03 [-20 8.467e-03 1.255e+00] + CP 49-4 1.311e+03 [-26 2.251e-03 1.643e+00] + CP 49-5 1.312e+03 [-33 4.286e-04 1.407e+00] + CP 49-6 1.313e+03 [-33 4.280e-04 7.040e-01] + CP 49-7 1.314e+03 [-36 2.018e-04 1.300e+00] + CP 49-8 1.315e+03 [-43 4.054e-05 1.084e+00] + CP 49-9 1.315e+03 [-50 8.983e-06 8.927e-01] + CP 49-10 1.316e+03 [-55 2.580e-06 1.242e+00] + CP 49-11 1.317e+03 [-67 1.822e-07 1.165e+00] + CP 49-12 1.318e+03 [-68 1.559e-07 6.305e-01] + CP 49-13 1.319e+03 [-70 9.437e-08 1.580e+00] + CP 49-14 1.320e+03 [-73 4.421e-08 1.076e+00] + CP 49-15 1.321e+03 [-81 6.354e-09 9.410e-01] + CP 49-16 1.322e+03 [-89 1.247e-09 1.169e+00] + CP 49-17 1.328e+03 [-103 4.562e-11 1.209e+00] + CP 50-1 1.329e+03 [1 1.582e+00 1.000e+00] + CP 50-2 1.330e+03 [-1 6.411e-01 1.347e+00] + CP 50-3 1.331e+03 [-12 5.983e-02 1.249e+00] + CP 50-4 1.332e+03 [-11 7.040e-02 5.844e-01] + CP 50-5 1.333e+03 [-13 4.503e-02 1.576e+00] + CP 50-6 1.334e+03 [-16 2.170e-02 1.065e+00] + CP 50-7 1.335e+03 [-25 2.586e-03 9.518e-01] + CP 50-8 1.336e+03 [-33 4.609e-04 1.156e+00] + CP 50-9 1.336e+03 [-46 2.446e-05 1.216e+00] + CP 50-10 1.337e+03 [-46 2.167e-05 2.000e+00] + CP 50-11 1.338e+03 [-48 1.411e-05 1.211e+00] + CP 50-12 1.339e+03 [-52 5.106e-06 8.898e-01] + CP 50-13 1.340e+03 [-59 1.172e-06 1.153e+00] + CP 50-14 1.341e+03 [-77 1.672e-08 1.148e+00] + CP 50-15 1.341e+03 [-72 5.042e-08 1.794e-01] + CP 50-16 1.342e+03 [-72 5.556e-08 1.691e+00] + CP 50-17 1.343e+03 [-74 3.429e-08 1.046e+00] + CP 50-18 1.344e+03 [-85 2.718e-09 9.690e-01] + CP 50-19 1.350e+03 [-93 4.358e-10 1.150e+00] + CP 50-20 1.355e+03 [-104 3.906e-11 1.186e+00] diff --git a/tests/cases/fsi/pipe_3d_partitioned/coupling_performance.png b/tests/cases/fsi/pipe_3d_partitioned/coupling_performance.png new file mode 100644 index 000000000..55f61a41a --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/coupling_performance.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58666e5e6a996d4a0479b4e0a3ac8ff51b6a02fb150ef4ddb226221f001b8e7b +size 298945 diff --git a/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_inlet.pdf b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_inlet.pdf new file mode 100644 index 000000000..75d161383 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_inlet.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_z.pdf new file mode 100644 index 000000000..f731e297f Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_disp_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_flow_rate.pdf b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_flow_rate.pdf new file mode 100644 index 000000000..94861207d Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_flow_rate.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_pressure_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_pressure_z.pdf new file mode 100644 index 000000000..d82ece98d Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_pressure_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_velocity_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_velocity_z.pdf new file mode 100644 index 000000000..6498af295 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/fluid_matching/compare_velocity_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/inlet_pressure_ramp.dat b/tests/cases/fsi/pipe_3d_partitioned/inlet_pressure_ramp.dat new file mode 100644 index 000000000..86742e44e --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/inlet_pressure_ramp.dat @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:565943a2ee83a05495ca119471c49456754117efaaeb8c99727f11b06edf7a9f +size 23 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-complete.mesh.vtu b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-complete.mesh.vtu new file mode 100644 index 000000000..8ddb1119b --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-complete.mesh.vtu @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bff21b54094fe528e6f7cd9b57dafc5863b530e112e08f8a62a04431ec14a7a3 +size 67969 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/end.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/end.vtp new file mode 100644 index 000000000..e7c6c313c --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/end.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57511367796bab30c07b3b5dbd3b6ec596e433cb1aae2afe763f05ab357655bf +size 13661 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/interface.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/interface.vtp new file mode 100644 index 000000000..c52d70d2a --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/interface.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c4a25e331d2c3ccbfa2ffcdd52e0ebbd37a8045989bec9abd7b2c0ed775e2b8 +size 28724 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/start.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/start.vtp new file mode 100644 index 000000000..55f41e4f7 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/fluid/mesh-surfaces/start.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d90d5fb5ce21d0889b27fd0a592aafa6121d19047bebd801710caf5cba6fb48a +size 13685 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-complete.mesh.vtu b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-complete.mesh.vtu new file mode 100644 index 000000000..08402c8eb --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-complete.mesh.vtu @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2527472b08ece18c95175c0661ca838f90ac8b97406d0c1c77459e94df620fe7 +size 36980 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/end.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/end.vtp new file mode 100644 index 000000000..4f2a81290 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/end.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb10cc3c374742b481031050fd6ceb2af30b8a14bd5627d89f930a4df01708a +size 12351 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/interface.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/interface.vtp new file mode 100644 index 000000000..9a8bafd82 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/interface.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9302a80f392f30ae07b98d0048cb7ad85f2fd3fcb0a86dbe50def69ba5a6cafc +size 28900 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/outside.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/outside.vtp new file mode 100644 index 000000000..1c5fbfe2e --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/outside.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29c18dc6c30b33a98016c3df4038433430307f05bc5fd3d2d39469b4a3d4bb63 +size 28813 diff --git a/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/start.vtp b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/start.vtp new file mode 100644 index 000000000..37fd38c98 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/mesh/solid/mesh-surfaces/start.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c679e42882a1a2db6382133d896879e84468058870863e670fc31212d4e29c2 +size 12335 diff --git a/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned.pvd b/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned.pvd new file mode 100644 index 000000000..f2e9be677 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned.pvd @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned/monolithic_vs_partitioned_0.vtp b/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned/monolithic_vs_partitioned_0.vtp new file mode 100644 index 000000000..0e3ef635d --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/monolithic_vs_partitioned/monolithic_vs_partitioned_0.vtp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32f736426163df2dade11b4ba7fc4b282313e261552c00ff2d5e521eb3acc8ab +size 49650 diff --git a/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_inlet.pdf b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_inlet.pdf new file mode 100644 index 000000000..28d6f1b4a Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_inlet.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_z.pdf new file mode 100644 index 000000000..37e2d8872 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_disp_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_flow_rate.pdf b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_flow_rate.pdf new file mode 100644 index 000000000..683e582eb Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_flow_rate.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_pressure_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_pressure_z.pdf new file mode 100644 index 000000000..5e94786b6 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_pressure_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_velocity_z.pdf b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_velocity_z.pdf new file mode 100644 index 000000000..8758eaef9 Binary files /dev/null and b/tests/cases/fsi/pipe_3d_partitioned/nice_try/compare_velocity_z.pdf differ diff --git a/tests/cases/fsi/pipe_3d_partitioned/pressure_comparison.png b/tests/cases/fsi/pipe_3d_partitioned/pressure_comparison.png new file mode 100644 index 000000000..70f733713 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/pressure_comparison.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57df244dde4c7dcad51ca691eb98d3b8e0a46c3821661c55d8bb31445c03e6ca +size 106515 diff --git a/tests/cases/fsi/pipe_3d_partitioned/result_fluid_010.vtu b/tests/cases/fsi/pipe_3d_partitioned/result_fluid_010.vtu new file mode 100644 index 000000000..76822e1ac --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/result_fluid_010.vtu @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d282281035c645b42df5100e533666a71593984446e6b79cde91ee7dfb4c5769 +size 88862 diff --git a/tests/cases/fsi/pipe_3d_partitioned/result_solid_010.vtu b/tests/cases/fsi/pipe_3d_partitioned/result_solid_010.vtu new file mode 100644 index 000000000..1ba745a9f --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/result_solid_010.vtu @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d4d02091f17e4f6a286f3e5beede38a679d242027e840bb633887af9aefe49a +size 26395 diff --git a/tests/cases/fsi/pipe_3d_partitioned/results.gif b/tests/cases/fsi/pipe_3d_partitioned/results.gif new file mode 100644 index 000000000..0c87216b0 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/results.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1f98ef6a06de4f38d40481ea5feb61f0a453cfe134b9afd1f6b04ffd3f6652d +size 3051594 diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver.xml b/tests/cases/fsi/pipe_3d_partitioned/solver.xml new file mode 100644 index 000000000..6f7c68fec --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver.xml @@ -0,0 +1,195 @@ + + + + + + + 0 + 3 + 5 + 1e-4 + 0.50 + STOP_SIM + true + result + 5 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + + + + false + 1 + 30 + 1e-4 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + + + false + 1 + 10 + 1e-6 + 0.3 + + + mesh + 0.0 + 1.0 + 0.3 + + + + + fsils + + 1e-12 + 400 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + + + 50 + 1e-4 + 0.5 + aitken + lumen_wall + wall_inner + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_50.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_50.xml new file mode 100644 index 000000000..4b0a1e42d --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_50.xml @@ -0,0 +1,195 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + + + + + + false + 1 + 30 + 1e-4 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + + + false + 1 + 10 + 1e-6 + 0.3 + + + + fsils + + 1e-12 + 400 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + + + 100 + 1e-6 + 0.01 + 1.0 + aitken + lumen_wall + wall_inner + solver_fluid.xml + solver_solid.xml + solver_mesh.xml + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_fluid.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid.xml new file mode 100644 index 000000000..aca5f90d0 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid.xml @@ -0,0 +1,78 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + true + result_fluid + 1 + 1 + 50 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + false + 1 + 10 + 1e-8 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_only.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_only.xml new file mode 100644 index 000000000..4df43350d --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_only.xml @@ -0,0 +1,77 @@ + + + + + + + 0 + 3 + 5 + 1e-4 + 0.50 + true + result_fluid + 5 + 1 + 5 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_ramp.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_ramp.xml new file mode 100644 index 000000000..841e1c3f8 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_ramp.xml @@ -0,0 +1,79 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + true + result_fluid + 1 + 1 + 50 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + false + 1 + 10 + 1e-8 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + Unsteady + inlet_pressure_ramp.dat + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_zero_pressure.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_zero_pressure.xml new file mode 100644 index 000000000..165699171 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_fluid_zero_pressure.xml @@ -0,0 +1,78 @@ + + + + + + + 0 + 3 + 1 + 1e-4 + 0.50 + false + result_test + 1 + 1 + 1 + 0 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_mesh.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_mesh.xml new file mode 100644 index 000000000..8d4ffd2f3 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_mesh.xml @@ -0,0 +1,88 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + true + result_mesh + 1 + 1 + 50 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + + lumen_wall + lumen_wall + + + + false + 1 + 10 + 1e-6 + 0.3 + + + mesh + 0.0 + 1.0 + 0.3 + + + + + fsils + + 1e-12 + 400 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_ramp.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_ramp.xml new file mode 100644 index 000000000..409831ca6 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_ramp.xml @@ -0,0 +1,198 @@ + + + + + + + 0 + 3 + 10 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + + + + + + false + 1 + 30 + 1e-4 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + + + false + 1 + 10 + 1e-6 + 0.3 + + + mesh + 0.0 + 1.0 + 0.3 + + + + + fsils + + 1e-12 + 400 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + + + 200 + 1e-4 + 0.01 + aitken + lumen_wall + wall_inner + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_solid.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_solid.xml new file mode 100644 index 000000000..31736ac6f --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_solid.xml @@ -0,0 +1,87 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + true + result_solid + 1 + 1 + 50 + 1 + 0 + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + false + 1 + 30 + 1e-6 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_solid_pressure.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_solid_pressure.xml new file mode 100644 index 000000000..eaf40386c --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_solid_pressure.xml @@ -0,0 +1,92 @@ + + + + + + + 0 + 3 + 100 + 1e-4 + 0.50 + true + result_solid_pressure + 10 + 1 + 100 + 1 + 0 + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + false + 1 + 30 + 1e-6 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Neu + 5.0e4 + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_solid_stiff.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_solid_stiff.xml new file mode 100644 index 000000000..7ccb38fcf --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_solid_stiff.xml @@ -0,0 +1,87 @@ + + + + + + + 0 + 3 + 50 + 1e-4 + 0.50 + true + result_solid + 1 + 1 + 50 + 1 + 0 + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + false + 1 + 30 + 1e-6 + + + struct + + M94 + 1.0 + 1.0e12 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/solver_stiff.xml b/tests/cases/fsi/pipe_3d_partitioned/solver_stiff.xml new file mode 100644 index 000000000..2b9091e23 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/solver_stiff.xml @@ -0,0 +1,194 @@ + + + + + + + 0 + 3 + 1 + 1e-4 + 0.50 + STOP_SIM + true + result + 1 + 1 + 1 + 0 + 1 + 0 + 0 + + + + mesh/fluid/mesh-complete.mesh.vtu + + mesh/fluid/mesh-surfaces/start.vtp + + + mesh/fluid/mesh-surfaces/end.vtp + + + mesh/fluid/mesh-surfaces/interface.vtp + + 0 + + + + mesh/solid/mesh-complete.mesh.vtu + + mesh/solid/mesh-surfaces/start.vtp + + + mesh/solid/mesh-surfaces/end.vtp + + + mesh/solid/mesh-surfaces/interface.vtp + + + mesh/solid/mesh-surfaces/outside.vtp + + 1 + + + + lumen_wall + + + + + false + 1 + 10 + 1e-6 + + + fluid + 1.0 + + 0.04 + + 0.2 + + + + + fsils + + 1e-14 + 200 + 100 + + + + true + true + + + + Neu + 5.0e4 + + + + + + + + + false + 1 + 30 + 1e-4 + + + struct + + M94 + 1.0 + 1.0e7 + 0.3 + + + + + fsils + + 1e-14 + 500 + 100 + + + + true + true + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + Dir + 0.0 + true + false + (0, 0, 1) + + + + + + + false + 1 + 10 + 1e-6 + 0.3 + + + + fsils + + 1e-12 + 400 + + + + true + + + + Dir + 0.0 + + + + Dir + 0.0 + + + + + + + 50 + 1e-10 + 0.01 + true + lumen_wall + wall_inner + solver_fluid.xml + solver_solid_stiff.xml + solver_mesh.xml + + + diff --git a/tests/cases/fsi/pipe_3d_partitioned/velocity_comparison.gif b/tests/cases/fsi/pipe_3d_partitioned/velocity_comparison.gif new file mode 100644 index 000000000..5122b4b47 --- /dev/null +++ b/tests/cases/fsi/pipe_3d_partitioned/velocity_comparison.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3420dc1d5867d29e296c784b53b21c4a30e1ce6c4ddab0454add3e957259d0d +size 1382121 diff --git a/tests/conftest.py b/tests/conftest.py index 7c4a36cab..fc8041d94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,7 @@ def n_proc(request): return request.param -def run_by_name(folder, name, t_max, n_proc=1): +def run_by_name(folder, name, t_max, n_proc=1, name_result=None): """ Run a test case and return results Args: @@ -53,6 +53,8 @@ def run_by_name(folder, name, t_max, n_proc=1): name: name of svMultiPhysics input file (.xml) t_max: time step to compare n_proc: number of processors + name_result: VTU filename to read from {n_proc}-procs/; defaults to + result_{t_max:03d}.vtu Returns: Simulation results @@ -105,9 +107,8 @@ def run_by_name(folder, name, t_max, n_proc=1): subprocess.call(cmd, cwd=folder, shell=True) # read results - fname = os.path.join( - folder, str(n_proc) + "-procs", "result_" + str(t_max).zfill(3) + ".vtu" - ) + result_file = name_result if name_result else "result_" + str(t_max).zfill(3) + ".vtu" + fname = os.path.join(folder, str(n_proc) + "-procs", result_file) if not os.path.exists(fname): raise RuntimeError("No svMultiPhysics output: " + fname) return meshio.read(fname) @@ -121,6 +122,7 @@ def run_with_reference( t_max=1, name_ref=None, name_inp="solver.xml", + name_result=None, ): """ Run a test case and compare it to a stored reference solution @@ -140,12 +142,12 @@ def run_with_reference( folder = os.path.join("cases", base_folder, test_folder) if is_not_Darwin: - res = run_by_name(folder, name_inp, t_max, n_proc) + res = run_by_name(folder, name_inp, t_max, n_proc, name_result) else: - if "petsc" in folder or "trilinos" in folder: + if "petsc" in folder or "trilinos" in folder: return else: - res = run_by_name(folder, name_inp, t_max, n_proc) + res = run_by_name(folder, name_inp, t_max, n_proc, name_result) # read reference fname = os.path.join(folder, name_ref) diff --git a/tests/test_fsi.py b/tests/test_fsi.py index dd383c9fa..6cc2cd89e 100644 --- a/tests/test_fsi.py +++ b/tests/test_fsi.py @@ -30,4 +30,20 @@ def test_pipe_3d_trilinos_ml(n_proc): def test_pipe_RCR_3d(n_proc): test_folder = "pipe_RCR_3d" t_max = 5 - run_with_reference(base_folder, test_folder, fields, n_proc, t_max) \ No newline at end of file + run_with_reference(base_folder, test_folder, fields, n_proc, t_max) + +def test_pipe_3d_partitioned_fluid(n_proc): + test_folder = "pipe_3d_partitioned" + t_max = 10 + run_with_reference(base_folder, test_folder, ["Velocity", "Pressure"], n_proc, t_max, + name_inp="solver_ramp.xml", + name_ref="result_fluid_010.vtu", + name_result="result_fluid_010.vtu") + +def test_pipe_3d_partitioned_solid(n_proc): + test_folder = "pipe_3d_partitioned" + t_max = 10 + run_with_reference(base_folder, test_folder, ["Displacement", "VonMises_stress"], n_proc, t_max, + name_inp="solver_ramp.xml", + name_ref="result_solid_010.vtu", + name_result="result_solid_010.vtu") \ No newline at end of file diff --git a/tests/unitTests/integrator_tests/test_fsi_coupling.cpp b/tests/unitTests/integrator_tests/test_fsi_coupling.cpp new file mode 100644 index 000000000..982499212 --- /dev/null +++ b/tests/unitTests/integrator_tests/test_fsi_coupling.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +/** + * @brief Integration tests for fsi_coupling namespace functions. + * + * Tests the FSI interface data exchange functions: extract_fluid_traction, + * extract_solid_displacement, apply_traction_on_solid, apply_displacement_on_mesh. + * + * Requires MPI and access to the FSI pipe_3d test case data files. + */ + +#include "gtest/gtest.h" + +#include "fsi_coupling.h" +#include "post.h" +#include "Integrator.h" +#include "Simulation.h" +#include "distribute.h" +#include "initialize.h" +#include "read_files.h" +#include "LinearAlgebra.h" +#include "set_bc.h" +#include "post.h" + +#include +#include +#include +#include + +#ifndef TEST_DATA_DIR +#define TEST_DATA_DIR "" +#endif + +// --------------------------------------------------------------------------- +// MPI environment (same as in test_step_equation.cpp -- only one takes effect) +// --------------------------------------------------------------------------- +class MPIEnvironment_FSICoupling : public ::testing::Environment { +public: + void SetUp() override { + int initialized = 0; + MPI_Initialized(&initialized); + if (!initialized) { + int argc = 0; + char** argv = nullptr; + MPI_Init(&argc, &argv); + } + } + void TearDown() override { + int finalized = 0; + MPI_Finalized(&finalized); + if (!finalized) { + MPI_Finalize(); + } + } +}; + +static testing::Environment* const mpi_env_fsi = + testing::AddGlobalTestEnvironment(new MPIEnvironment_FSICoupling); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +static void add_eq_la(ComMod& com_mod, eqType& lEq) +{ + lEq.linear_algebra = LinearAlgebraFactory::create_interface(lEq.linear_algebra_type); + lEq.linear_algebra->set_preconditioner(lEq.linear_algebra_preconditioner); + lEq.linear_algebra->initialize(com_mod, lEq); + if (lEq.linear_algebra_assembly_type != consts::LinearAlgebraType::none) { + lEq.linear_algebra->set_assembly(lEq.linear_algebra_assembly_type); + } +} + +static Simulation* setup_fsi_simulation() +{ + std::string fsi_dir = std::string(TEST_DATA_DIR) + "/fsi/pipe_3d"; + char orig_dir[4096]; + getcwd(orig_dir, sizeof(orig_dir)); + chdir(fsi_dir.c_str()); + + auto sim = new Simulation(); + read_files_ns::read_files(sim, "solver.xml"); + distribute(sim); + Vector init_time(3); + initialize(sim, init_time); + for (int iEq = 0; iEq < sim->com_mod.nEq; iEq++) { + add_eq_la(sim->com_mod, sim->com_mod.eq[iEq]); + } + + chdir(orig_dir); + return sim; +} + +static void run_one_fsi_timestep(Simulation* sim) +{ + auto& com_mod = sim->com_mod; + auto& integrator = sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + + com_mod.cTS += 1; + com_mod.time += com_mod.dt; + com_mod.cEq = 0; + for (auto& eq : com_mod.eq) { + eq.itr = 0; + eq.ok = false; + } + + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + integrator.step(); + + solutions.old.get_acceleration() = solutions.current.get_acceleration(); + solutions.old.get_velocity() = solutions.current.get_velocity(); + if (com_mod.dFlag) { + solutions.old.get_displacement() = solutions.current.get_displacement(); + } +} + +static void teardown_sim(Simulation* sim) +{ + for (int iEq = 0; iEq < sim->com_mod.nEq; iEq++) { + sim->com_mod.eq[iEq].linear_algebra->finalize(); + } + delete sim; +} + +static bool test_data_available() +{ + std::string path = std::string(TEST_DATA_DIR); + if (path.empty()) return false; + struct stat st; + return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); +} + +// Find a face by name in the simulation meshes +static const faceType* find_face(const ComMod& com_mod, const std::string& name) +{ + for (int iM = 0; iM < com_mod.nMsh; iM++) { + for (int iFa = 0; iFa < com_mod.msh[iM].nFa; iFa++) { + if (com_mod.msh[iM].fa[iFa].name == name) { + return &com_mod.msh[iM].fa[iFa]; + } + } + } + return nullptr; +} + +static const mshType* find_mesh_for_face(const ComMod& com_mod, const std::string& face_name) +{ + for (int iM = 0; iM < com_mod.nMsh; iM++) { + for (int iFa = 0; iFa < com_mod.msh[iM].nFa; iFa++) { + if (com_mod.msh[iM].fa[iFa].name == face_name) { + return &com_mod.msh[iM]; + } + } + } + return nullptr; +} + +// =========================================================================== +// Tests +// =========================================================================== + + +/// @brief Extract solid displacement from a converged FSI solution. +TEST(FSICoupling, ExtractSolidDisplacement) +{ + if (!test_data_available()) GTEST_SKIP() << "Test data not available"; + + auto sim = setup_fsi_simulation(); + auto& com_mod = sim->com_mod; + const int nsd = com_mod.nsd; + + // Run one time step to get a non-trivial solution + run_one_fsi_timestep(sim); + + auto& solutions = sim->get_integrator().get_solutions(); + auto& eq = com_mod.eq[0]; // FSI equation + + auto* solid_face = find_face(com_mod, "wall_inner"); + ASSERT_NE(solid_face, nullptr); + + // Extract displacement + auto disp = fsi_coupling::extract_solid_displacement(com_mod, eq, *solid_face, solutions); + + // Verify against direct solution array access + const auto& Dn = solutions.current.get_displacement(); + int s = eq.s; + double max_diff = 0.0; + for (int a = 0; a < solid_face->nNo; a++) { + int Ac = solid_face->gN(a); + for (int i = 0; i < nsd; i++) { + double diff = std::abs(disp(i, a) - Dn(i + s, Ac)); + if (diff > max_diff) max_diff = diff; + } + } + EXPECT_LT(max_diff, 1e-14) + << "Extracted displacement should match solution array"; + + // Verify displacement is non-zero (problem has deformation) + double max_val = 0.0; + for (int a = 0; a < solid_face->nNo; a++) { + for (int i = 0; i < nsd; i++) { + double v = std::abs(disp(i, a)); + if (v > max_val) max_val = v; + } + } + EXPECT_GT(max_val, 1e-10) << "Displacement should be non-zero after FSI solve"; + + teardown_sim(sim); +} + +/// @brief Extract fluid traction and verify total force is reasonable. +TEST(FSICoupling, ExtractFluidTraction) +{ + if (!test_data_available()) GTEST_SKIP() << "Test data not available"; + + auto sim = setup_fsi_simulation(); + auto& com_mod = sim->com_mod; + const int nsd = com_mod.nsd; + + // Run one time step + run_one_fsi_timestep(sim); + + auto& integrator = sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + auto& eq = com_mod.eq[0]; // FSI equation + + auto* fluid_face = find_face(com_mod, "lumen_wall"); + auto* fluid_mesh = find_mesh_for_face(com_mod, "lumen_wall"); + ASSERT_NE(fluid_face, nullptr); + ASSERT_NE(fluid_mesh, nullptr); + + // Extract consistent nodal traction forces + com_mod.cEq = 0; // ensure correct equation is active + auto traction = post::compute_face_traction( + com_mod, sim->cm_mod, *fluid_mesh, *fluid_face, eq, solutions); + + // Check dimensions + EXPECT_EQ(traction.nrows(), nsd); + EXPECT_EQ(traction.ncols(), fluid_face->nNo); + + // Compute total force (sum of consistent nodal forces) + Vector total_force(nsd); + for (int a = 0; a < fluid_face->nNo; a++) { + for (int i = 0; i < nsd; i++) { + total_force(i) += traction(i, a); + } + } + + // The total force should be non-zero (pressure-driven flow in a pipe) + double force_mag = 0.0; + for (int i = 0; i < nsd; i++) { + force_mag += total_force(i) * total_force(i); + } + force_mag = sqrt(force_mag); + EXPECT_GT(force_mag, 1e-10) + << "Total traction force should be non-zero for pressure-driven FSI flow"; + + teardown_sim(sim); +} + diff --git a/tests/unitTests/integrator_tests/test_partitioned_fsi.cpp b/tests/unitTests/integrator_tests/test_partitioned_fsi.cpp new file mode 100644 index 000000000..d247df529 --- /dev/null +++ b/tests/unitTests/integrator_tests/test_partitioned_fsi.cpp @@ -0,0 +1,564 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +/** + * @brief Sanity checks for partitioned FSI coupling. + * + * Tests traction extraction, sign consistency, velocity/displacement + * consistency, data transfer, and predictor restore. + */ + +#include "gtest/gtest.h" + +#include "fsi_coupling.h" +#include "post.h" +#include "Integrator.h" +#include "Simulation.h" +#include "distribute.h" +#include "initialize.h" +#include "read_files.h" +#include "LinearAlgebra.h" +#include "set_bc.h" +#include "all_fun.h" + +#include +#include +#include +#include + +#ifndef TEST_DATA_DIR +#define TEST_DATA_DIR "" +#endif + +// --------------------------------------------------------------------------- +// MPI environment +// --------------------------------------------------------------------------- +class MPIEnvironment_PartFSI : public ::testing::Environment { +public: + void SetUp() override { + int initialized = 0; + MPI_Initialized(&initialized); + if (!initialized) { + int argc = 0; + char** argv = nullptr; + MPI_Init(&argc, &argv); + } + } + void TearDown() override { + int finalized = 0; + MPI_Finalized(&finalized); + if (!finalized) { + MPI_Finalize(); + } + } +}; + +static testing::Environment* const mpi_env_pfsi = + testing::AddGlobalTestEnvironment(new MPIEnvironment_PartFSI); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +void add_eq_linear_algebra_test(ComMod& com_mod, eqType& lEq) +{ + lEq.linear_algebra = LinearAlgebraFactory::create_interface(lEq.linear_algebra_type); + lEq.linear_algebra->set_preconditioner(lEq.linear_algebra_preconditioner); + lEq.linear_algebra->initialize(com_mod, lEq); + if (lEq.linear_algebra_assembly_type != consts::LinearAlgebraType::none) { + lEq.linear_algebra->set_assembly(lEq.linear_algebra_assembly_type); + } +} + +static bool pfsi_test_data_available() +{ + std::string path = std::string(TEST_DATA_DIR); + if (path.empty()) return false; + struct stat st; + return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); +} + +static const faceType* pfsi_find_face(const ComMod& com_mod, const std::string& name) +{ + for (int iM = 0; iM < com_mod.nMsh; iM++) + for (int iFa = 0; iFa < com_mod.msh[iM].nFa; iFa++) + if (com_mod.msh[iM].fa[iFa].name == name) + return &com_mod.msh[iM].fa[iFa]; + return nullptr; +} + +static const mshType* pfsi_find_mesh(const ComMod& com_mod, const std::string& face_name) +{ + for (int iM = 0; iM < com_mod.nMsh; iM++) + for (int iFa = 0; iFa < com_mod.msh[iM].nFa; iFa++) + if (com_mod.msh[iM].fa[iFa].name == face_name) + return &com_mod.msh[iM]; + return nullptr; +} + +struct SimSetup { + Simulation* sim; + char orig_dir[4096]; + + SimSetup(const std::string& case_dir, const std::string& xml_file) { + getcwd(orig_dir, sizeof(orig_dir)); + std::string full_dir = std::string(TEST_DATA_DIR) + "/" + case_dir; + chdir(full_dir.c_str()); + + sim = new Simulation(); + read_files_ns::read_files(sim, xml_file); + distribute(sim); + Vector init_time(3); + initialize(sim, init_time); + for (int iEq = 0; iEq < sim->com_mod.nEq; iEq++) + add_eq_linear_algebra_test(sim->com_mod, sim->com_mod.eq[iEq]); + } + + ~SimSetup() { + for (int iEq = 0; iEq < sim->com_mod.nEq; iEq++) + sim->com_mod.eq[iEq].linear_algebra->finalize(); + delete sim; + chdir(orig_dir); + } + + void run_one_timestep() { + auto& com_mod = sim->com_mod; + auto& integrator = sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + + com_mod.cTS += 1; + com_mod.time += com_mod.dt; + com_mod.cEq = 0; + for (auto& eq : com_mod.eq) { eq.itr = 0; eq.ok = false; } + + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + integrator.step(); + + solutions.old.get_acceleration() = solutions.current.get_acceleration(); + solutions.old.get_velocity() = solutions.current.get_velocity(); + if (com_mod.dFlag) + solutions.old.get_displacement() = solutions.current.get_displacement(); + } +}; + +// =========================================================================== +// Test 1: Traction sign and magnitude at lumen_wall +// =========================================================================== +TEST(PartitionedFSI, TractionSignAndMagnitude) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + SimSetup fsi("fsi/pipe_3d", "solver.xml"); + auto& com_mod = fsi.sim->com_mod; + auto& cm_mod = fsi.sim->cm_mod; + auto& integrator = fsi.sim->get_integrator(); + const int nsd = com_mod.nsd; + + // Run 1 time step of monolithic FSI + fsi.run_one_timestep(); + + // Find lumen_wall face and fluid mesh + auto* wall_face = pfsi_find_face(com_mod, "lumen_wall"); + auto* fluid_mesh = pfsi_find_mesh(com_mod, "lumen_wall"); + ASSERT_NE(wall_face, nullptr); + ASSERT_NE(fluid_mesh, nullptr); + + // Extract traction at lumen_wall + auto traction = post::compute_face_traction( + com_mod, cm_mod, *fluid_mesh, *wall_face, com_mod.eq[0], + integrator.get_solutions()); + + // Sum all nodal forces → total force vector + double total_force[3] = {0, 0, 0}; + for (int a = 0; a < wall_face->nNo; a++) + for (int i = 0; i < nsd; i++) + total_force[i] += traction(i, a); + + // Total radial force: project each nodal force onto radial direction + double total_radial = 0.0; + for (int a = 0; a < wall_face->nNo; a++) { + int Ac = wall_face->gN(a); + double x = com_mod.x(0, Ac); + double y = com_mod.x(1, Ac); + double r = sqrt(x*x + y*y); + if (r < 1e-10) continue; + total_radial += (x * traction(0, a) + y * traction(1, a)) / r; + } + + // Compute mean pressure at wall from Yg + auto& Yg = integrator.get_Yg(); + double sum_p = 0.0; + for (int a = 0; a < wall_face->nNo; a++) { + int Ac = wall_face->gN(a); + sum_p += Yg(nsd, Ac); // pressure is DOF nsd + } + double mean_p = sum_p / wall_face->nNo; + double wall_area = wall_face->area; + + // Expected radial force ≈ mean_pressure * wall_area + double expected_radial = mean_p * wall_area; + + std::cout << " Wall area: " << wall_area << std::endl; + std::cout << " Mean wall pressure: " << mean_p << std::endl; + std::cout << " Expected radial: " << expected_radial << std::endl; + std::cout << " Actual radial: " << total_radial << std::endl; + std::cout << " Ratio (act/exp): " << total_radial / expected_radial << std::endl; + std::cout << " Total axial force: " << total_force[2] << std::endl; + + // Traction should be radially outward (positive) + EXPECT_GT(total_radial, 0.0) + << "Traction should point radially outward for positive pressure"; + + // Radial force should be within 50% of pressure*area estimate + // (viscous contribution and non-uniform pressure cause deviation) + EXPECT_NEAR(total_radial / expected_radial, 1.0, 0.5) + << "Total radial force should be ~pressure*area"; +} + +// =========================================================================== +// Test 2: Traction at inlet matches Neumann BC +// =========================================================================== +TEST(PartitionedFSI, TractionMatchesNeumannBC) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + SimSetup fluid("fsi/pipe_3d_partitioned", "solver_fluid_only.xml"); + auto& com_mod = fluid.sim->com_mod; + auto& cm_mod = fluid.sim->cm_mod; + auto& integrator = fluid.sim->get_integrator(); + const int nsd = com_mod.nsd; + + // Run 1 time step + fluid.run_one_timestep(); + + // Find inlet face + auto* inlet_face = pfsi_find_face(com_mod, "lumen_inlet"); + auto* lumen_mesh = pfsi_find_mesh(com_mod, "lumen_inlet"); + ASSERT_NE(inlet_face, nullptr); + ASSERT_NE(lumen_mesh, nullptr); + + // Extract traction at inlet + auto traction = post::compute_face_traction( + com_mod, cm_mod, *lumen_mesh, *inlet_face, com_mod.eq[0], + integrator.get_solutions()); + + // Sum axial (z) component of traction at inlet + double total_axial = 0.0; + for (int a = 0; a < inlet_face->nNo; a++) + total_axial += traction(2, a); + + // Get inlet area and mean pressure + double inlet_area = inlet_face->area; + auto& Yg = integrator.get_Yg(); + double sum_p = 0.0; + for (int a = 0; a < inlet_face->nNo; a++) { + int Ac = inlet_face->gN(a); + sum_p += Yg(nsd, Ac); + } + double mean_p = sum_p / inlet_face->nNo; + + // The Neumann BC is pressure = 5e4 + double applied_pressure = 5.0e4; + double expected_force = mean_p * inlet_area; + + std::cout << " Inlet area: " << inlet_area << std::endl; + std::cout << " Mean inlet pressure: " << mean_p << std::endl; + std::cout << " Applied Neumann BC: " << applied_pressure << std::endl; + std::cout << " Total axial traction:" << total_axial << std::endl; + std::cout << " Expected (p*A): " << expected_force << std::endl; + std::cout << " Ratio (act/exp): " << total_axial / expected_force << std::endl; + + // Axial traction at inlet should be on the order of p*A + // Sign: extract_fluid_traction returns force ON THE SOLID. + // At the inlet, the normal points inward (into the pipe, -z direction + // for a pipe from z=0 to z=L). So the traction should push in +z direction + // if the inlet normal is -z (sigma.n with n=-z gives +p in +z). + // Actually the sign depends on whether the face normal points in or out. + EXPECT_NE(total_axial, 0.0) << "Inlet traction should be non-zero"; + + // The ratio should be close to -1 or +1 depending on normal convention + double ratio = total_axial / expected_force; + std::cout << " |Ratio|: " << std::abs(ratio) << std::endl; + EXPECT_NEAR(std::abs(ratio), 1.0, 0.5) + << "Total axial traction should be ~pressure*area"; +} + +// =========================================================================== +// Test 3: apply_traction_on_solid sign check +// =========================================================================== +TEST(PartitionedFSI, TractionApplicationSign) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + SimSetup solid("fsi/pipe_3d_partitioned", "solver_solid.xml"); + auto& com_mod = solid.sim->com_mod; + const int nsd = com_mod.nsd; + + // Find wall_inner face + auto* inner_face = pfsi_find_face(com_mod, "wall_inner"); + ASSERT_NE(inner_face, nullptr); + + // Run predictor to set up solution arrays + solid.sim->get_integrator().predictor(); + + // Allocate R sized for the solid equation + com_mod.cEq = 0; + auto& eq = com_mod.eq[0]; + + // Create a known traction: unit radially outward force at each node + Array traction(nsd, inner_face->nNo); + for (int a = 0; a < inner_face->nNo; a++) { + int Ac = inner_face->gN(a); + double x = com_mod.x(0, Ac); + double y = com_mod.x(1, Ac); + double r = sqrt(x*x + y*y); + if (r < 1e-10) r = 1.0; + // Unit radially outward force + traction(0, a) = x / r; + traction(1, a) = y / r; + traction(2, a) = 0.0; + } + + // Zero R, then apply traction + com_mod.R.resize(eq.dof, com_mod.tnNo); + com_mod.R = 0.0; + + fsi_coupling::apply_traction_on_solid(com_mod, eq, *inner_face, traction); + + // Check: R should be NEGATIVE (R -= traction, traction is positive outward) + double sum_radial_R = 0.0; + for (int a = 0; a < inner_face->nNo; a++) { + int Ac = inner_face->gN(a); + double x = com_mod.x(0, Ac); + double y = com_mod.x(1, Ac); + double r = sqrt(x*x + y*y); + if (r < 1e-10) continue; + double radial_R = (x * com_mod.R(0, Ac) + y * com_mod.R(1, Ac)) / r; + sum_radial_R += radial_R; + } + + // R -= traction → R should be negative for positive (outward) traction + EXPECT_LT(sum_radial_R, 0.0) + << "R should be negative after applying outward traction (R -= traction)"; + + std::cout << " Sum of radial R at interface: " << sum_radial_R << std::endl; + std::cout << " (Should be negative: R -= outward_traction)" << std::endl; +} + +// =========================================================================== +// Test 4: Solid velocity/displacement Newmark consistency +// =========================================================================== +TEST(PartitionedFSI, SolidNewmarkConsistency) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + SimSetup solid("fsi/pipe_3d_partitioned", "solver_solid.xml"); + auto& com_mod = solid.sim->com_mod; + auto& integrator = solid.sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + auto& eq = com_mod.eq[0]; + const int nsd = com_mod.nsd; + const double dt = com_mod.dt; + const double gam = eq.gam; + + // Run 1 time step (solid with zero loading → should stay at zero) + solid.run_one_timestep(); + + // The solid has Dir BCs at inlet/outlet but no loading, + // so Dn ≈ 0, Yn ≈ 0, An ≈ 0 everywhere. + // Check Newmark consistency: Yn = Yo + dt*((1-gam)*Ao + gam*An) + auto& An = solutions.current.get_acceleration(); + auto& Yn = solutions.current.get_velocity(); + auto& Ao = solutions.old.get_acceleration(); + auto& Yo = solutions.old.get_velocity(); + + double max_err = 0.0; + for (int Ac = 0; Ac < com_mod.tnNo; Ac++) { + for (int i = 0; i < nsd; i++) { + double yn_expected = Yo(i, Ac) + dt * ((1.0 - gam) * Ao(i, Ac) + gam * An(i, Ac)); + double err = std::abs(Yn(i, Ac) - yn_expected); + max_err = std::max(max_err, err); + } + } + + std::cout << " Max Newmark consistency error (Yn vs formula): " << max_err << std::endl; + EXPECT_LT(max_err, 1e-10) + << "Velocity should be consistent with Newmark formula"; +} + +// =========================================================================== +// Test 7: Predictor restore verification +// =========================================================================== +TEST(PartitionedFSI, PredictorRestore) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + SimSetup fluid("fsi/pipe_3d_partitioned", "solver_fluid_only.xml"); + auto& com_mod = fluid.sim->com_mod; + auto& integrator = fluid.sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + const int nsd = com_mod.nsd; + + // Run predictor + com_mod.cTS = 1; + com_mod.time = com_mod.dt; + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + + // Save predictor state + Array saved_An(solutions.current.get_acceleration()); + Array saved_Yn(solutions.current.get_velocity()); + Array saved_Dn(solutions.current.get_displacement()); + Array saved_x(com_mod.x); + + // Run one Newton solve (modifies An, Yn, Dn) + for (auto& eq : com_mod.eq) { eq.itr = 0; eq.ok = false; } + integrator.step(); + + // Verify solution changed + double max_change = 0.0; + auto& Yn = solutions.current.get_velocity(); + for (int a = 0; a < Yn.ncols(); a++) + for (int i = 0; i < Yn.nrows(); i++) + max_change = std::max(max_change, std::abs(Yn(i, a) - saved_Yn(i, a))); + EXPECT_GT(max_change, 0.0) << "Solution should change after Newton solve"; + + // Restore predictor state + solutions.current.get_acceleration() = saved_An; + solutions.current.get_velocity() = saved_Yn; + solutions.current.get_displacement() = saved_Dn; + com_mod.x = saved_x; + + // Verify restoration is exact + double max_restore_err = 0.0; + auto& restored_Yn = solutions.current.get_velocity(); + for (int a = 0; a < restored_Yn.ncols(); a++) + for (int i = 0; i < restored_Yn.nrows(); i++) + max_restore_err = std::max(max_restore_err, + std::abs(restored_Yn(i, a) - saved_Yn(i, a))); + + EXPECT_EQ(max_restore_err, 0.0) << "Predictor restore should be bitwise exact"; + + double max_x_err = 0.0; + for (int a = 0; a < com_mod.x.ncols(); a++) + for (int i = 0; i < nsd; i++) + max_x_err = std::max(max_x_err, std::abs(com_mod.x(i, a) - saved_x(i, a))); + EXPECT_EQ(max_x_err, 0.0) << "Mesh coordinate restore should be bitwise exact"; + + std::cout << " Max change after solve: " << max_change << std::endl; + std::cout << " Max restore error (Yn): " << max_restore_err << std::endl; + std::cout << " Max restore error (x): " << max_x_err << std::endl; +} + +// =========================================================================== +// Test: Prescribed wall velocity produces correct mass conservation +// =========================================================================== +TEST(PartitionedFSI, WallVelocityMassConservation) +{ + if (!pfsi_test_data_available()) GTEST_SKIP() << "Test data not available"; + + // Use zero-pressure fluid + SimSetup fluid("fsi/pipe_3d_partitioned", "solver_fluid_zero_pressure.xml"); + auto& com_mod = fluid.sim->com_mod; + auto& integrator = fluid.sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + const int nsd = com_mod.nsd; + + auto* wall_face = pfsi_find_face(com_mod, "lumen_wall"); + auto* inlet_face = pfsi_find_face(com_mod, "lumen_inlet"); + auto* outlet_face = pfsi_find_face(com_mod, "lumen_outlet"); + ASSERT_NE(wall_face, nullptr); + ASSERT_NE(inlet_face, nullptr); + ASSERT_NE(outlet_face, nullptr); + + // Advance one time step + com_mod.cTS = 1; + com_mod.time = com_mod.dt; + for (auto& eq : com_mod.eq) { eq.itr = 0; eq.ok = false; } + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + + // Prescribe uniform radial wall velocity v_wall = 1.0 + double v_wall = 1.0; + Array wall_vel(nsd, wall_face->nNo); + for (int a = 0; a < wall_face->nNo; a++) { + int Ac = wall_face->gN(a); + double x = com_mod.x(0, Ac); + double y = com_mod.x(1, Ac); + double r = sqrt(x*x + y*y); + if (r < 1e-10) r = 1.0; + wall_vel(0, a) = v_wall * x / r; // radial outward + wall_vel(1, a) = v_wall * y / r; + wall_vel(2, a) = 0.0; // no axial + } + + fsi_coupling::apply_velocity_on_fluid( + com_mod, com_mod.eq[0], *wall_face, wall_vel, solutions); + + // Solve fluid + integrator.step_equation(0, [&]() { + set_bc::enforce_dirichlet_dofs_on_face(com_mod, *wall_face, 0, nsd); + }); + + // Check: is the wall velocity preserved after the solve? + auto& Yn = solutions.current.get_velocity(); + double max_wall_err = 0.0; + for (int a = 0; a < wall_face->nNo; a++) { + int Ac = wall_face->gN(a); + for (int i = 0; i < nsd; i++) { + double err = std::abs(Yn(i, Ac) - wall_vel(i, a)); + max_wall_err = std::max(max_wall_err, err); + } + } + std::cout << " Max wall velocity error after solve: " << max_wall_err << std::endl; + EXPECT_LT(max_wall_err, 1e-6) + << "Wall velocity should be preserved by enforce_dirichlet_dofs_on_face"; + + // Check mass conservation: integral(v·n) over all boundaries = 0 + // Wall: v·n = v_wall (radially outward, n points outward) + // Inlet/outlet: v·n determined by the solve + + // Compute flow through inlet (v_z integrated over inlet area) + double inlet_flow = 0.0; + for (int a = 0; a < inlet_face->nNo; a++) { + int Ac = inlet_face->gN(a); + // nV includes area weighting; just sum v_z for average + inlet_flow += Yn(2, Ac); + } + inlet_flow /= inlet_face->nNo; // mean axial velocity at inlet + + // Compute flow through outlet + double outlet_flow = 0.0; + for (int a = 0; a < outlet_face->nNo; a++) { + int Ac = outlet_face->gN(a); + outlet_flow += Yn(2, Ac); + } + outlet_flow /= outlet_face->nNo; + + // Wall volume flux: dV/dt = 2*pi*r*L*v_wall + // For r=1.0, L≈0.5: dV/dt ≈ pi ≈ 3.14 + // This should equal the net axial flow through inlet+outlet + double pipe_r = 1.0; + // Get pipe length from z range + double z_min = 1e30, z_max = -1e30; + for (int a = 0; a < com_mod.tnNo; a++) { + z_min = std::min(z_min, com_mod.x(2, a)); + z_max = std::max(z_max, com_mod.x(2, a)); + } + double pipe_L = z_max - z_min; + double wall_flux = 2.0 * M_PI * pipe_r * pipe_L * v_wall; + double inlet_area = inlet_face->area; + + std::cout << " Pipe: r=" << pipe_r << " L=" << pipe_L << std::endl; + std::cout << " Wall volume flux (2*pi*r*L*v_wall): " << wall_flux << std::endl; + std::cout << " Inlet area: " << inlet_area << std::endl; + std::cout << " Mean inlet v_z: " << inlet_flow << std::endl; + std::cout << " Mean outlet v_z: " << outlet_flow << std::endl; + std::cout << " Expected inlet v_z (wall_flux/inlet_area): " << wall_flux / inlet_area << std::endl; + + // The wall expansion should drive flow out through the open ends + // With Neu BC at inlet (p=0) and natural BC at outlet, + // the flow distribution depends on the geometry + EXPECT_NE(inlet_flow, 0.0) + << "Wall velocity should drive flow through the inlet"; +} diff --git a/tests/unitTests/integrator_tests/test_step_equation.cpp b/tests/unitTests/integrator_tests/test_step_equation.cpp new file mode 100644 index 000000000..07472b79e --- /dev/null +++ b/tests/unitTests/integrator_tests/test_step_equation.cpp @@ -0,0 +1,304 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +/** + * @brief Integration tests for Integrator::step_equation() + * + * Tests that step_equation() produces correct results by comparing against + * the monolithic step() method on real solver problems. + * + * These tests require MPI and access to test case data files. + * They are skipped if the test data directory is not available. + */ + +#include "gtest/gtest.h" + +#include "Integrator.h" +#include "Simulation.h" +#include "distribute.h" +#include "initialize.h" +#include "read_files.h" +#include "LinearAlgebra.h" +#include "set_bc.h" + +#include +#include +#include +#include + +// Path to test data, defined via CMake +#ifndef TEST_DATA_DIR +#define TEST_DATA_DIR "" +#endif + +// --------------------------------------------------------------------------- +// MPI environment: initializes MPI once before all tests, finalizes after +// --------------------------------------------------------------------------- +class MPIEnvironment : public ::testing::Environment { +public: + void SetUp() override { + int initialized = 0; + MPI_Initialized(&initialized); + if (!initialized) { + int argc = 0; + char** argv = nullptr; + MPI_Init(&argc, &argv); + } + } + void TearDown() override { + int finalized = 0; + MPI_Finalized(&finalized); + if (!finalized) { + MPI_Finalize(); + } + } +}; + +// Register the MPI environment (runs before any test) +static testing::Environment* const mpi_env = + testing::AddGlobalTestEnvironment(new MPIEnvironment); + +// --------------------------------------------------------------------------- +// Helper: set up a full simulation from an XML file +// --------------------------------------------------------------------------- +static Simulation* setup_simulation(const std::string& xml_path) +{ + // The solver reads mesh files relative to the XML directory, + // so we must chdir there before calling read_files. + std::string dir = xml_path.substr(0, xml_path.find_last_of('/')); + std::string file = xml_path.substr(xml_path.find_last_of('/') + 1); + char orig_dir[4096]; + getcwd(orig_dir, sizeof(orig_dir)); + chdir(dir.c_str()); + + auto simulation = new Simulation(); + read_files_ns::read_files(simulation, file); + distribute(simulation); + Vector init_time(3); + initialize(simulation, init_time); + for (int iEq = 0; iEq < simulation->com_mod.nEq; iEq++) { + add_eq_linear_algebra(simulation->com_mod, simulation->com_mod.eq[iEq]); + } + + chdir(orig_dir); + return simulation; +} + +static void teardown_simulation(Simulation* simulation) +{ + for (int iEq = 0; iEq < simulation->com_mod.nEq; iEq++) { + simulation->com_mod.eq[iEq].linear_algebra->finalize(); + } + delete simulation; +} + +// --------------------------------------------------------------------------- +// Helper: run one time step using step() +// --------------------------------------------------------------------------- +static void run_one_timestep_step(Simulation* simulation) +{ + auto& com_mod = simulation->com_mod; + auto& integrator = simulation->get_integrator(); + auto& solutions = integrator.get_solutions(); + + com_mod.cTS += 1; + com_mod.time += com_mod.dt; + com_mod.cEq = 0; + for (auto& eq : com_mod.eq) { + eq.itr = 0; + eq.ok = false; + } + + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + integrator.step(); + + // Copy current -> old for next step + solutions.old.get_acceleration() = solutions.current.get_acceleration(); + solutions.old.get_velocity() = solutions.current.get_velocity(); + if (com_mod.dFlag) { + solutions.old.get_displacement() = solutions.current.get_displacement(); + } + com_mod.cplBC.xo = com_mod.cplBC.xn; +} + +// --------------------------------------------------------------------------- +// Helper: run one time step using step_equation() per equation +// --------------------------------------------------------------------------- +static void run_one_timestep_step_equation(Simulation* simulation, + int outer_iters = 1) +{ + auto& com_mod = simulation->com_mod; + auto& integrator = simulation->get_integrator(); + auto& solutions = integrator.get_solutions(); + + com_mod.cTS += 1; + com_mod.time += com_mod.dt; + com_mod.cEq = 0; + for (auto& eq : com_mod.eq) { + eq.itr = 0; + eq.ok = false; + } + + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + + for (int outer = 0; outer < outer_iters; outer++) { + for (int iEq = 0; iEq < com_mod.nEq; iEq++) { + integrator.step_equation(iEq); + } + } + + // Copy current -> old for next step + solutions.old.get_acceleration() = solutions.current.get_acceleration(); + solutions.old.get_velocity() = solutions.current.get_velocity(); + if (com_mod.dFlag) { + solutions.old.get_displacement() = solutions.current.get_displacement(); + } + com_mod.cplBC.xo = com_mod.cplBC.xn; +} + +// --------------------------------------------------------------------------- +// Helper: compute relative difference between two solution arrays +// --------------------------------------------------------------------------- +static double rel_diff(const Array& a, const Array& b) +{ + double max_diff = 0.0; + double max_val = 0.0; + for (int j = 0; j < a.ncols(); j++) { + for (int i = 0; i < a.nrows(); i++) { + double d = std::abs(a(i,j) - b(i,j)); + if (d > max_diff) max_diff = d; + double v = std::abs(a(i,j)); + if (v > max_val) max_val = v; + } + } + return (max_val > 0) ? max_diff / max_val : max_diff; +} + +// --------------------------------------------------------------------------- +// Helper: check if test data directory exists +// --------------------------------------------------------------------------- +static bool test_data_available() +{ + std::string path = std::string(TEST_DATA_DIR); + if (path.empty()) return false; + struct stat st; + return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); +} + +// =========================================================================== +// Tests +// =========================================================================== + +/// @brief For a single-equation problem, step_equation(0) must produce +/// bit-identical results to step(). +TEST(StepEquation, SingleEquationMatchesStep) +{ + if (!test_data_available()) GTEST_SKIP() << "Test data not available"; + + std::string xml = std::string(TEST_DATA_DIR) + "/fluid/newtonian/solver.xml"; + + // Run with step() + auto sim_a = setup_simulation(xml); + run_one_timestep_step(sim_a); + Array Yn_step = sim_a->get_integrator().get_solutions().current.get_velocity(); + teardown_simulation(sim_a); + + // Run with step_equation(0) + auto sim_b = setup_simulation(xml); + run_one_timestep_step_equation(sim_b, 1); + Array Yn_step_eq = sim_b->get_integrator().get_solutions().current.get_velocity(); + teardown_simulation(sim_b); + + double diff = rel_diff(Yn_step, Yn_step_eq); + EXPECT_LT(diff, 1e-12) << "step_equation(0) should match step() for single-equation problems"; +} + +/// @brief For coupled FSI, sequential step_equation converges to step() +/// result when outer coupling iterations are added. +TEST(StepEquation, CoupledFSIConvergesWithOuterIterations) +{ + if (!test_data_available()) GTEST_SKIP() << "Test data not available"; + + std::string xml = std::string(TEST_DATA_DIR) + "/fsi/pipe_3d/solver.xml"; + + // Run with step() as reference + auto sim_ref = setup_simulation(xml); + run_one_timestep_step(sim_ref); + Array Yn_ref = sim_ref->get_integrator().get_solutions().current.get_velocity(); + Array Dn_ref = sim_ref->get_integrator().get_solutions().current.get_displacement(); + teardown_simulation(sim_ref); + + // Run with 1 outer iteration (single pass) - should have coupling error + auto sim_1 = setup_simulation(xml); + run_one_timestep_step_equation(sim_1, 1); + Array Yn_1 = sim_1->get_integrator().get_solutions().current.get_velocity(); + Array Dn_1 = sim_1->get_integrator().get_solutions().current.get_displacement(); + teardown_simulation(sim_1); + + double diff_vel_1 = rel_diff(Yn_ref, Yn_1); + double diff_disp_1 = rel_diff(Dn_ref, Dn_1); + + // Run with 4 outer iterations - should converge to step() result + auto sim_4 = setup_simulation(xml); + run_one_timestep_step_equation(sim_4, 4); + Array Yn_4 = sim_4->get_integrator().get_solutions().current.get_velocity(); + Array Dn_4 = sim_4->get_integrator().get_solutions().current.get_displacement(); + teardown_simulation(sim_4); + + double diff_vel_4 = rel_diff(Yn_ref, Yn_4); + double diff_disp_4 = rel_diff(Dn_ref, Dn_4); + + // With 4 outer iterations, the coupling error should be orders of magnitude + // smaller than with 1 iteration + EXPECT_LT(diff_vel_4, diff_vel_1 * 1e-4) + << "4 outer iterations should reduce velocity coupling error by >4 orders"; + EXPECT_LT(diff_disp_4, diff_disp_1 * 1e-4) + << "4 outer iterations should reduce displacement coupling error by >4 orders"; + + // With 4 outer iterations, the result should match step() to near machine precision + EXPECT_LT(diff_vel_4, 1e-10) + << "Velocity should match step() after 4 outer iterations"; + EXPECT_LT(diff_disp_4, 1e-10) + << "Displacement should match step() after 4 outer iterations"; +} + +/// @brief The post_assembly callback fires on every Newton iteration. +TEST(StepEquation, PostAssemblyCallbackFires) +{ + if (!test_data_available()) GTEST_SKIP() << "Test data not available"; + + std::string xml = std::string(TEST_DATA_DIR) + "/fluid/newtonian/solver.xml"; + + auto sim = setup_simulation(xml); + auto& com_mod = sim->com_mod; + auto& integrator = sim->get_integrator(); + auto& solutions = integrator.get_solutions(); + + // Set up time step + com_mod.cTS += 1; + com_mod.time += com_mod.dt; + com_mod.cEq = 0; + for (auto& eq : com_mod.eq) { + eq.itr = 0; + eq.ok = false; + } + + integrator.predictor(); + set_bc::set_bc_dir(com_mod, solutions); + + // Count callback invocations + int callback_count = 0; + integrator.step_equation(0, [&callback_count]() { + callback_count++; + }); + + // Callback should fire once per Newton iteration (at least minItr times) + EXPECT_GE(callback_count, com_mod.eq[0].minItr) + << "Callback should fire at least minItr times"; + EXPECT_LE(callback_count, com_mod.eq[0].maxItr) + << "Callback should fire at most maxItr times"; + + teardown_simulation(sim); +}