From 26dbb1e46f8743e379eb4517f63245d7cb66dc5e Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:36:49 -0700 Subject: [PATCH 01/51] Add 3D-1D coupling via svOneDSolver shared library interface --- Code/Source/solver/CMakeLists.txt | 2 + Code/Source/solver/ComMod.cpp | 11 + Code/Source/solver/ComMod.h | 27 ++ Code/Source/solver/Parameters.cpp | 31 ++ Code/Source/solver/Parameters.h | 43 ++- Code/Source/solver/baf_ini.cpp | 5 + Code/Source/solver/read_files.cpp | 11 +- Code/Source/solver/set_bc.cpp | 7 + .../sv1D_interface/OneDSolverInterface.cpp | 108 +++++++ .../sv1D_interface/OneDSolverInterface.h | 86 ++++++ Code/Source/solver/sv1D_subroutines.cpp | 282 ++++++++++++++++++ Code/Source/solver/sv1D_subroutines.h | 25 ++ Code/Source/solver/txt.cpp | 5 + 13 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp create mode 100644 Code/Source/solver/sv1D_interface/OneDSolverInterface.h create mode 100644 Code/Source/solver/sv1D_subroutines.cpp create mode 100644 Code/Source/solver/sv1D_subroutines.h diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index c546c2822..14863afea 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -204,6 +204,7 @@ set(CSRCS stokes.h stokes.cpp sv_struct.h sv_struct.cpp svZeroD_interface.h svZeroD_interface.cpp + sv1D_subroutines.h sv1D_subroutines.cpp txt.h txt.cpp utils.h utils.cpp ustruct.h ustruct.cpp @@ -228,6 +229,7 @@ set(CSRCS SPLIT.c svZeroD_interface/LPNSolverInterface.h svZeroD_interface/LPNSolverInterface.cpp + sv1D_interface/OneDSolverInterface.h sv1D_interface/OneDSolverInterface.cpp BoundaryCondition.h BoundaryCondition.cpp RobinBoundaryCondition.h RobinBoundaryCondition.cpp diff --git a/Code/Source/solver/ComMod.cpp b/Code/Source/solver/ComMod.cpp index 0ac2d4365..a9110b67f 100644 --- a/Code/Source/solver/ComMod.cpp +++ b/Code/Source/solver/ComMod.cpp @@ -217,3 +217,14 @@ void svZeroDSolverInterfaceType::set_data(const svZeroDSolverInterfaceParameters has_data = true; } + +void sv1DSolverInterfaceType::set_data(const svOneDSolverInterfaceParameters& params) +{ + if (!params.defined()) { + return; + } + + solver_library = params.shared_library(); + input_file = params.input_file(); + has_data = true; +} diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index 8b41e5c6a..b64d10427 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -807,6 +807,28 @@ class svZeroDSolverInterfaceType void set_data(const svZeroDSolverInterfaceParameters& params); }; +//---------------------------- +// sv1DSolverInterfaceType +//---------------------------- +// This class stores information used to interface to the svOneDSolver. +// +class sv1DSolverInterfaceType +{ + public: + // Path to the 1D solver shared library (without .so/.dylib extension, + // or with extension if the full path is provided). + std::string solver_library; + + // Path to the 1D solver .in input file. + std::string input_file; + + // If the data has been set for the interface. Set to true after + // the svOneDSolver_interface XML element has been parsed. + bool has_data = false; + + void set_data(const svOneDSolverInterfaceParameters& params); +}; + /// @brief For coupled 0D-3D problems // class cplBCType @@ -822,6 +844,9 @@ class cplBCType // Whether to use svZeroD bool useSvZeroD = false; + // Whether to use sv1D (svOneDSolver) + bool useSv1D = false; + // Whether to initialize RCR from flow data bool initRCR = false; @@ -853,6 +878,8 @@ class cplBCType svZeroDSolverInterfaceType svzerod_solver_interface; + sv1DSolverInterfaceType sv1d_solver_interface; + /// @brief The name of history file containing "X" std::string saveName; //std::string(LEN=stdL) :: saveName = "LPN.dat"; diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 38ec0fe5d..138378fc5 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -1175,6 +1175,34 @@ void svZeroDSolverInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem value_set = true; } +////////////////////////////////////////////////////////// +// svOneDSolverInterfaceParameters // +////////////////////////////////////////////////////////// + +const std::string svOneDSolverInterfaceParameters::xml_element_name_ = "svOneDSolver_interface"; + +svOneDSolverInterfaceParameters::svOneDSolverInterfaceParameters() +{ + bool required = true; + + set_parameter("Coupling_type", "", required, coupling_type); + set_parameter("Shared_library", "", required, shared_library); + set_parameter("Input_file", "", required, input_file); +} + +void svOneDSolverInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) +{ + std::string error_msg = "Unknown svOneDSolver_interface XML element '"; + + using std::placeholders::_1; + using std::placeholders::_2; + std::function ftpr = + std::bind(&svOneDSolverInterfaceParameters::set_parameter_value, *this, _1, _2); + xml_util_set_parameters(ftpr, xml_elem, error_msg); + + value_set = true; +} + ////////////////////////////////////////////////////////// // OutputParameters // ////////////////////////////////////////////////////////// @@ -2415,6 +2443,9 @@ void EquationParameters::set_values(tinyxml2::XMLElement* eq_elem, DomainParamet } else if (name == svZeroDSolverInterfaceParameters::xml_element_name_) { svzerodsolver_interface_parameters.set_values(item); + } else if (name == svOneDSolverInterfaceParameters::xml_element_name_) { + svonedsolver_interface_parameters.set_values(item); + } else if (name == DomainParameters::xml_element_name_) { auto domain_params = new DomainParameters(); domain_params->set_values(item); diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index b726ba47f..c3a8650b3 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -679,7 +679,46 @@ class svZeroDSolverInterfaceParameters : public ParameterLists bool value_set = false; }; -/// @brief Body force over a mesh using the "Add_BF" command. +//---------------------------------- +// svOneDSolverInterfaceParameters +//---------------------------------- +/// @brief Parameters for coupling to the svOneDSolver (1D blood-flow solver). +/// +/// XML element: \code {.xml} +/// +/// Implicit +/// /path/to/libsvoned_interface +/// /path/to/solver.in +/// +/// \endcode +/// +/// Notes +/// ----- +/// Coupling_type: "Explicit" | "Implicit" | "Semi-implicit" +/// Controls how the 3D Newton iteration couples to the 1D solver +/// (same semantics as the 0D coupling_type). +/// Shared_library: Path to the 1D interface shared library. The +/// extension (.so or .dylib) may be omitted; it will be appended +/// automatically based on the platform. +/// Input_file: Path to the svOneDSolver .in input file. +// +class svOneDSolverInterfaceParameters : public ParameterLists +{ + public: + svOneDSolverInterfaceParameters(); + + static const std::string xml_element_name_; + + bool defined() const { return value_set; }; + void set_values(tinyxml2::XMLElement* xml_elem); + + Parameter coupling_type; + Parameter shared_library; + Parameter input_file; + + bool value_set = false; +}; + /// /// \code {.xml} /// @@ -1528,6 +1567,8 @@ class EquationParameters : public ParameterLists svZeroDSolverInterfaceParameters svzerodsolver_interface_parameters; + svOneDSolverInterfaceParameters svonedsolver_interface_parameters; + DomainParameters* default_domain = nullptr; std::vector domains; diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index 0c6201f42..4edc2658f 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -12,6 +12,7 @@ #include "set_bc.h" #include "utils.h" #include "svZeroD_interface.h" +#include "sv1D_subroutines.h" #include "fsils_api.hpp" #include "fils_struct.hpp" @@ -162,6 +163,10 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) svZeroD::init_svZeroD(com_mod, cm_mod); } + if (com_mod.cplBC.useSv1D) { + sv1D::init_sv1D(com_mod, cm_mod); + } + // Initialize cap integration for Coupled boundary conditions for (int iEq = 0; iEq < com_mod.nEq; iEq++) { auto& eq = com_mod.eq[iEq]; diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 65b9aaaf1..64a1fb059 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -1425,9 +1425,15 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) cplBC.useSvZeroD = true; cplbc_type_str = eq_params->svzerodsolver_interface_parameters.coupling_type.value(); cplBC.svzerod_solver_interface.set_data(eq_params->svzerodsolver_interface_parameters); + } else if (eq_params->svonedsolver_interface_parameters.defined()) { + cplBC.useSv1D = true; + cplbc_type_str = eq_params->svonedsolver_interface_parameters.coupling_type.value(); + cplBC.sv1d_solver_interface.set_data(eq_params->svonedsolver_interface_parameters); } - if (eq_params->couple_to_genBC.defined() || eq_params->svzerodsolver_interface_parameters.defined()) { + if (eq_params->couple_to_genBC.defined() || + eq_params->svzerodsolver_interface_parameters.defined() || + eq_params->svonedsolver_interface_parameters.defined()) { try { cplBC.schm = consts::cplbc_name_to_type.at(cplbc_type_str); } catch (const std::out_of_range& exception) { @@ -1446,6 +1452,9 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) } else if (cplBC.useSvZeroD) { cplBC.nX = 0; + + } else if (cplBC.useSv1D) { + cplBC.nX = 0; } } } diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 5277f6009..67a9967a2 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -18,6 +18,7 @@ #include "utils.h" #include #include "svZeroD_interface.h" +#include "sv1D_subroutines.h" namespace set_bc { @@ -173,6 +174,8 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + } else if (cplBC.useSv1D) { + sv1D::calc_sv1D(com_mod, cm_mod, 'D'); } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } @@ -221,6 +224,8 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + } else if (cplBC.useSv1D) { + sv1D::calc_sv1D(com_mod, cm_mod, 'D'); } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } @@ -793,6 +798,8 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD){ svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + } else if (cplBC.useSv1D) { + sv1D::calc_sv1D(com_mod, cm_mod, 'D'); } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } diff --git a/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp b/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp new file mode 100644 index 000000000..b211b1021 --- /dev/null +++ b/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#include "OneDSolverInterface.h" + +#include +#include +#include + +OneDSolverInterface::~OneDSolverInterface() +{ + if (library_handle_) { + dlclose(library_handle_); + library_handle_ = nullptr; + } +} + +void OneDSolverInterface::load_library(const std::string& interface_lib) +{ + library_handle_ = dlopen(interface_lib.c_str(), RTLD_LAZY); + if (!library_handle_) { + throw std::runtime_error(std::string("[OneDSolverInterface] Could not load shared library '") + + interface_lib + "': " + dlerror()); + } + + // Clear any existing error. + dlerror(); + + auto load_sym = [&](const char* name) -> void* { + void* sym = dlsym(library_handle_, name); + const char* err = dlerror(); + if (err) { + throw std::runtime_error(std::string("[OneDSolverInterface] Could not load symbol '") + + name + "': " + err); + } + return sym; + }; + + *(void**)(&initialize_1d_) = load_sym("initialize_1d"); + *(void**)(&set_external_step_size_1d_) = load_sym("set_external_step_size_1d"); + *(void**)(&return_1d_solution_) = load_sym("return_1d_solution"); + *(void**)(&update_1d_solution_) = load_sym("update_1d_solution"); + *(void**)(&run_1d_simulation_step_1d_) = load_sym("run_1d_simulation_step_1d"); + *(void**)(&extract_coupled_dof_) = load_sym("extract_coupled_dof"); +} + +void OneDSolverInterface::initialize(const std::string& input_file, + int& problem_id, + int& system_size, + const std::string& coupling_type) +{ + if (!initialize_1d_) { + throw std::runtime_error("[OneDSolverInterface] initialize_1d not loaded"); + } + initialize_1d_(input_file.c_str(), problem_id, system_size, + coupling_type.c_str()); + problem_id_ = problem_id; + system_size_ = system_size; +} + +void OneDSolverInterface::set_external_step_size(int problem_id, double dt) +{ + if (!set_external_step_size_1d_) { + throw std::runtime_error("[OneDSolverInterface] set_external_step_size_1d not loaded"); + } + set_external_step_size_1d_(problem_id, dt); +} + +void OneDSolverInterface::return_solution(int problem_id, double* solution, int size) +{ + if (!return_1d_solution_) { + throw std::runtime_error("[OneDSolverInterface] return_1d_solution not loaded"); + } + return_1d_solution_(problem_id, solution, size); +} + +void OneDSolverInterface::update_solution(int problem_id, double* solution, int size) +{ + if (!update_1d_solution_) { + throw std::runtime_error("[OneDSolverInterface] update_1d_solution not loaded"); + } + update_1d_solution_(problem_id, solution, size); +} + +void OneDSolverInterface::run_step(int problem_id, double current_time, + int save_flag, + const std::string& coupling_type, + double* params, double* solution, + double& cpl_value, int& error_code) +{ + if (!run_1d_simulation_step_1d_) { + throw std::runtime_error("[OneDSolverInterface] run_1d_simulation_step_1d not loaded"); + } + run_1d_simulation_step_1d_(problem_id, current_time, save_flag, + coupling_type.c_str(), params, solution, + cpl_value, error_code); +} + +void OneDSolverInterface::extract_coupled_dof(int problem_id, int& coupled_dof, + const std::string& coupling_type) +{ + if (!extract_coupled_dof_) { + throw std::runtime_error("[OneDSolverInterface] extract_coupled_dof not loaded"); + } + // The shared-library signature uses a non-const char* buffer. + std::string buf = coupling_type; + extract_coupled_dof_(problem_id, coupled_dof, &buf[0]); +} diff --git a/Code/Source/solver/sv1D_interface/OneDSolverInterface.h b/Code/Source/solver/sv1D_interface/OneDSolverInterface.h new file mode 100644 index 000000000..974589d38 --- /dev/null +++ b/Code/Source/solver/sv1D_interface/OneDSolverInterface.h @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef ONEDSOLVER_INTERFACE_H +#define ONEDSOLVER_INTERFACE_H + +#include +#include +#include + +/// @brief Wrapper class for dynamically loading and calling the 1D solver shared library. +/// +/// This class uses dlopen/dlsym to load the libsvoned_interface shared library at runtime +/// and provides C++ method wrappers for each of its exported C functions. +/// +/// Shared library functions: +/// - initialize_1d(input_file, problem_id, system_size, coupling_type) +/// - set_external_step_size_1d(problem_id, dt) +/// - return_1d_solution(problem_id, solution, size) +/// - update_1d_solution(problem_id, solution, size) +/// - run_1d_simulation_step_1d(problem_id, time, save_flag, coupling_type, +/// params, solution, cpl_value, error_code) +/// - extract_coupled_dof(problem_id, coupled_dof, coupling_type) +// +class OneDSolverInterface { + public: + OneDSolverInterface() = default; + ~OneDSolverInterface(); + + /// @brief Load the 1D solver shared library from the given path. + void load_library(const std::string& interface_lib); + + /// @brief Initialize the 1D solver from an input file. + /// @param input_file Path to the 1D solver .in file. + /// @param problem_id Output: problem identifier assigned by the solver. + /// @param system_size Output: total number of DOFs (nodes * 2: flow + area). + /// @param coupling_type "NEU" or "DIR" coupling direction. + void initialize(const std::string& input_file, int& problem_id, + int& system_size, const std::string& coupling_type); + + /// @brief Synchronize the 1D solver's internal time step with the 3D solver. + void set_external_step_size(int problem_id, double dt); + + /// @brief Copy the current 1D solution into the caller-provided buffer. + void return_solution(int problem_id, double* solution, int size); + + /// @brief Push a solution vector into the 1D solver as the current state. + void update_solution(int problem_id, double* solution, int size); + + /// @brief Advance the 1D solver by one time step. + /// @param problem_id Problem identifier. + /// @param current_time Current simulation time (start of the step). + /// @param save_flag Non-zero to write VTK output for this step. + /// @param coupling_type "NEU" or "DIR". + /// @param params Array [N, t1, t2, ..., val1, val2, ...] where N=2. + /// @param solution In/out: solution vector updated after the step. + /// @param cpl_value Output: the BC value returned by the 1D solver + /// (pressure for NEU, flow for DIR). + /// @param error_code Output: non-zero on failure. + void run_step(int problem_id, double current_time, int save_flag, + const std::string& coupling_type, double* params, + double* solution, double& cpl_value, int& error_code); + + /// @brief Retrieve the index within the solution vector that corresponds to + /// the coupled boundary DOF. + void extract_coupled_dof(int problem_id, int& coupled_dof, + const std::string& coupling_type); + + // Public data members set after initialize(). + int problem_id_ = 0; + int system_size_ = 0; + + private: + void* library_handle_ = nullptr; + + // Function pointers to shared-library symbols. + void (*initialize_1d_)(const char*, int&, int&, const char*) = nullptr; + void (*set_external_step_size_1d_)(int, double) = nullptr; + void (*return_1d_solution_)(int, double*, int) = nullptr; + void (*update_1d_solution_)(int, double*, int) = nullptr; + void (*run_1d_simulation_step_1d_)(int, double, int, const char*, double*, + double*, double&, int&) = nullptr; + void (*extract_coupled_dof_)(int, int&, char*) = nullptr; +}; + +#endif // ONEDSOLVER_INTERFACE_H diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp new file mode 100644 index 000000000..22dfc6f20 --- /dev/null +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +// 3D-1D coupling subroutines. +// +// These routines interface the 3D finite-element solver (svMultiPhysics) with +// the 1D blood-flow solver (svOneDSolver) via a dynamically loaded shared +// library (libsvoned_interface.so/.dylib). +// +// Coupling overview +// ----------------- +// NEU coupling (1D inlet driven by 3D outflow): +// 3D → 1D : flow rate Q (as params[3..4]) +// 1D → 3D : pressure P (as cplBCvalue) +// BC applied on 3D face : Neumann (pressure traction) +// +// DIR coupling (1D outlet driven by 3D pressure): +// 3D → 1D : pressure P (as params[3..4]) +// 1D → 3D : flow rate Q (as cplBCvalue) +// BC applied on 3D face : Dirichlet (velocity profile) +// +// params array passed to run_1d_simulation_step_1d_: +// params[0] = 2.0 (number of time points) +// params[1] = t_old (time at start of step) +// params[2] = t_new (time at end of step) +// params[3] = BC_val_old (Q or P at t_old) +// params[4] = BC_val_new (Q or P at t_new) + +#include "sv1D_subroutines.h" + +#include +#include +#include +#include +#include +#include + +#include "ComMod.h" +#include "consts.h" +#include "utils.h" +#include "sv1D_interface/OneDSolverInterface.h" + +namespace sv1D { + +// --------------------------------------------------------------------------- +// Module-level state (one 1D solver instance per simulation). +// --------------------------------------------------------------------------- + +static OneDSolverInterface* oned_interface = nullptr; + +// Problem identifier assigned by the 1D library during initialization. +static int oned_model_id = 0; + +// Total DOF count of the 1D solution vector (nodes * 2: flow + area/pressure). +static int oned_system_size = 0; + +// "NEU" or "DIR" — determined from the face BC type at initialization time. +static std::string oned_coupling_type; + +// Index within the 1D solution vector that corresponds to the coupled DOF. +static int oned_coupled_dof = 0; + +// 1D solution vector maintained by the 3D solver. +// This is the authoritative snapshot used to restore the 1D state before +// each call to run_step(). +static std::vector oned_solution; + +// Simulation time tracked for the 1D solver (advanced only on 'L' steps). +static double sv1DTime = 0.0; + +// --------------------------------------------------------------------------- +// Helper: find the first BC that uses cplBC.fa (iBC_cpl) and sv1D. +// Returns {ptr, bGrp} or throws. +// --------------------------------------------------------------------------- +static std::pair find_1d_face(const ComMod& com_mod) +{ + using namespace consts; + const int iEq = 0; + const auto& eq = com_mod.eq[iEq]; + for (int iBc = 0; iBc < eq.nBc; iBc++) { + const auto& bc = eq.bc[iBc]; + int ptr = bc.cplBCptr; + if (ptr == -1) continue; + if (!utils::btest(bc.bType, iBC_cpl)) continue; + return {ptr, com_mod.cplBC.fa[ptr].bGrp}; + } + throw std::runtime_error("[sv1D::find_1d_face] No 1D-coupled face found in eq[0]."); +} + +// --------------------------------------------------------------------------- +// init_sv1D +// --------------------------------------------------------------------------- +void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) +{ + using namespace consts; + + auto& cplBC = com_mod.cplBC; + auto& solver_if = cplBC.sv1d_solver_interface; + auto& cm = com_mod.cm; + + if (!solver_if.has_data) { + throw std::runtime_error("[sv1D::init_sv1D] sv1D solver interface data is missing."); + } + + // Determine the coupling direction (NEU or DIR) from the BC type of the + // first coupled face. Only master needs this but we resolve it globally. + auto [ptr, bGrp] = find_1d_face(com_mod); + oned_coupling_type = (bGrp == CplBCType::cplBC_Neu) ? "NEU" : "DIR"; + + if (cm.mas(cm_mod)) { + // ----- Load shared library ----- + const std::string lib_base = solver_if.solver_library; + // Try .so first (Linux), then .dylib (macOS). + std::string lib_path; + { + std::ifstream f_so(lib_base + ".so"); + std::ifstream f_dy(lib_base + ".dylib"); + if (f_so.good()) { + lib_path = lib_base + ".so"; + } else if (f_dy.good()) { + lib_path = lib_base + ".dylib"; + } else { + // Try using the path as-is (already includes extension). + lib_path = lib_base; + } + } + + oned_interface = new OneDSolverInterface(); + oned_interface->load_library(lib_path); + + // ----- Initialize 1D solver ----- + int problem_id = 0; + int system_size = 0; + oned_interface->initialize(solver_if.input_file, problem_id, + system_size, oned_coupling_type); + oned_model_id = problem_id; + oned_system_size = system_size; + + // ----- Synchronize time step ----- + oned_interface->set_external_step_size(oned_model_id, com_mod.dt); + + // ----- Retrieve coupled DOF index ----- + oned_interface->extract_coupled_dof(oned_model_id, oned_coupled_dof, + oned_coupling_type); + + // ----- Copy initial 1D solution to local buffer ----- + oned_solution.resize(oned_system_size, 0.0); + oned_interface->return_solution(oned_model_id, oned_solution.data(), + oned_system_size); + + // ----- Set initial cplBC.fa[ptr].y ----- + // For NEU: initial BC value is the pressure at the coupled node. + // For DIR: initial BC value is the flow at the coupled node. + // The coupled DOF index is oned_coupled_dof (0-based) in the solution + // vector, where even indices = flow, odd indices = area or pressure + // depending on the 1D formulation. The shared library returns the + // correct cplBCvalue via run_step, so we initialise y = 0 here and let + // the first calc_sv1D call set the real value. + cplBC.fa[ptr].y = 0.0; + } + + // Broadcast system_size and coupling metadata to slave processes. + cm.bcast(cm_mod, &oned_system_size); + cm.bcast(cm_mod, &oned_coupled_dof); + + // Slave processes also allocate the solution buffer (unused but kept for + // any future parallel extension). + if (cm.slv(cm_mod)) { + oned_solution.resize(oned_system_size, 0.0); + } + + // Run one 'D' step so that the initial resistance term bc.r is populated + // before the first Newton iteration (matches the svZeroD pattern). + if (cplBC.schm != CplBCType::cplBC_E) { + // Flowrate/pressure are 0 at t=0; calc_sv1D handles this gracefully. + calc_sv1D(com_mod, cm_mod, 'D'); + } +} + +// --------------------------------------------------------------------------- +// calc_sv1D +// --------------------------------------------------------------------------- +void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) +{ + using namespace consts; + + auto& cplBC = com_mod.cplBC; + auto& cm = com_mod.cm; + + // Only the master process drives the 1D solver. + if (cm.mas(cm_mod)) { + const int iEq = 0; + auto& eq = com_mod.eq[iEq]; + + double t_old = sv1DTime; + double t_new = sv1DTime + com_mod.dt; + + // Save solution so that perturbation steps can be undone. + std::vector solution_snapshot = oned_solution; + + for (int iBc = 0; iBc < eq.nBc; iBc++) { + auto& bc = eq.bc[iBc]; + int ptr = bc.cplBCptr; + if (ptr == -1) continue; + if (!utils::btest(bc.bType, iBC_cpl)) continue; + + // Build params = [2, t_old, t_new, BC_val_old, BC_val_new] + double params[5]; + params[0] = 2.0; + params[1] = t_old; + params[2] = t_new; + + if (cplBC.fa[ptr].bGrp == CplBCType::cplBC_Neu) { + // NEU: 3D provides flow Q → 1D returns pressure P. + params[3] = cplBC.fa[ptr].Qo; + params[4] = cplBC.fa[ptr].Qn; + } else { + // DIR: 3D provides pressure P → 1D returns flow Q. + params[3] = cplBC.fa[ptr].Po; + params[4] = cplBC.fa[ptr].Pn; + } + + // Use a working copy of the solution so that perturbation steps + // ('D') do not corrupt the committed state in oned_solution. + std::vector work_sol = oned_solution; + + // Push the stored (committed) solution into the 1D solver. + oned_interface->update_solution(oned_model_id, work_sol.data(), + oned_system_size); + + // Save flag: write VTK output only on the final 'L' step. + int save_flag = (BCFlag == 'L') ? 1 : 0; + + double cpl_value = 0.0; + int error_code = 0; + + oned_interface->run_step(oned_model_id, t_old, save_flag, + oned_coupling_type, params, + work_sol.data(), cpl_value, error_code); + + if (error_code != 0) { + throw std::runtime_error("[sv1D::calc_sv1D] 1D solver step failed with error code " + + std::to_string(error_code)); + } + + // Store the returned BC value. + cplBC.fa[ptr].y = cpl_value; + + // On the final iteration only, commit the updated solution so that + // the next time step starts from the correct state. + if (BCFlag == 'L') { + oned_solution = work_sol; + } + } + + // Advance the 1D simulation clock on the final iteration. + if (BCFlag == 'L') { + sv1DTime += com_mod.dt; + } + } + + // Broadcast updated cplBC.fa[i].y to all parallel processes. + if (!cm.seq()) { + Vector y(cplBC.nFa); + + if (cm.mas(cm_mod)) { + for (int i = 0; i < cplBC.nFa; i++) { + y(i) = cplBC.fa[i].y; + } + } + + cm.bcast(cm_mod, y); + + if (cm.slv(cm_mod)) { + for (int i = 0; i < cplBC.nFa; i++) { + cplBC.fa[i].y = y(i); + } + } + } +} + +} // namespace sv1D diff --git a/Code/Source/solver/sv1D_subroutines.h b/Code/Source/solver/sv1D_subroutines.h new file mode 100644 index 000000000..e368dcfb5 --- /dev/null +++ b/Code/Source/solver/sv1D_subroutines.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) Stanford University, The Regents of the University of California, and others. +// SPDX-License-Identifier: BSD-3-Clause + +#ifndef SV1D_SUBROUTINES_H +#define SV1D_SUBROUTINES_H + +#include "Simulation.h" +#include "consts.h" +#include "sv1D_interface/OneDSolverInterface.h" + +namespace sv1D { + +/// @brief Initialize the 1D solver and populate the initial cplBC state. +/// Called once from baf_ini() after the BC data structures are set up. +void init_sv1D(ComMod& com_mod, const CmMod& cm_mod); + +/// @brief Advance the 1D solver by one time step and update the coupled BC value. +/// +/// @param BCFlag 'D' - derivative / perturbation step (state is NOT committed). +/// 'L' - last Newton iteration (state IS committed, time advances). +void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag); + +} // namespace sv1D + +#endif // SV1D_SUBROUTINES_H diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index 462cd8281..c9d56fce2 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -13,6 +13,7 @@ #include "utils.h" #include #include "svZeroD_interface.h" +#include "sv1D_subroutines.h" namespace txt_ns { @@ -176,6 +177,10 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so set_bc::genBC_Integ_X(com_mod, cm_mod, "L"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'L'); + + } else if (cplBC.useSv1D) { + sv1D::calc_sv1D(com_mod, cm_mod, 'L'); + } else { for (auto& bc : com_mod.eq[0].bc) { if (utils::btest(bc.bType, iBC_RCR)) { From 9512433c02f2ebed4085a8b8dec8d0f3bbf9f389 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:27 -0700 Subject: [PATCH 02/51] Fix code review issues: remove unused variable, safer char* buffer, clarify time argument --- .../Source/solver/sv1D_interface/OneDSolverInterface.cpp | 9 ++++++--- Code/Source/solver/sv1D_subroutines.cpp | 7 ++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp b/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp index b211b1021..bae1c9ec1 100644 --- a/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp +++ b/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp @@ -6,6 +6,7 @@ #include #include #include +#include OneDSolverInterface::~OneDSolverInterface() { @@ -102,7 +103,9 @@ void OneDSolverInterface::extract_coupled_dof(int problem_id, int& coupled_dof, if (!extract_coupled_dof_) { throw std::runtime_error("[OneDSolverInterface] extract_coupled_dof not loaded"); } - // The shared-library signature uses a non-const char* buffer. - std::string buf = coupling_type; - extract_coupled_dof_(problem_id, coupled_dof, &buf[0]); + // Copy into a mutable buffer; the shared-library function signature uses + // char* (not const char*) so we must pass a writable copy. + std::vector buf(coupling_type.begin(), coupling_type.end()); + buf.push_back('\0'); + extract_coupled_dof_(problem_id, coupled_dof, buf.data()); } diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index 22dfc6f20..6b1864272 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -195,9 +195,6 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) double t_old = sv1DTime; double t_new = sv1DTime + com_mod.dt; - // Save solution so that perturbation steps can be undone. - std::vector solution_snapshot = oned_solution; - for (int iBc = 0; iBc < eq.nBc; iBc++) { auto& bc = eq.bc[iBc]; int ptr = bc.cplBCptr; @@ -205,6 +202,8 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (!utils::btest(bc.bType, iBC_cpl)) continue; // Build params = [2, t_old, t_new, BC_val_old, BC_val_new] + // params[1..2] carry the time window; t_old is also passed as the + // standalone current_time argument required by the 1D library API. double params[5]; params[0] = 2.0; params[1] = t_old; @@ -223,8 +222,6 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Use a working copy of the solution so that perturbation steps // ('D') do not corrupt the committed state in oned_solution. std::vector work_sol = oned_solution; - - // Push the stored (committed) solution into the 1D solver. oned_interface->update_solution(oned_model_id, work_sol.data(), oned_system_size); From a5f1b56aecd3531c55dfe09ba3c4743ee3fb6b50 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:28 -0700 Subject: [PATCH 03/51] Redesign 1D coupling for parallel per-model execution across MPI ranks --- Code/Source/solver/ComMod.cpp | 1 - Code/Source/solver/ComMod.h | 11 +- Code/Source/solver/Parameters.cpp | 8 +- Code/Source/solver/Parameters.h | 10 +- Code/Source/solver/baf_ini.cpp | 5 + Code/Source/solver/read_files.cpp | 25 +- Code/Source/solver/sv1D_subroutines.cpp | 313 ++++++++++++------------ 7 files changed, 201 insertions(+), 172 deletions(-) diff --git a/Code/Source/solver/ComMod.cpp b/Code/Source/solver/ComMod.cpp index a9110b67f..d9b0b676c 100644 --- a/Code/Source/solver/ComMod.cpp +++ b/Code/Source/solver/ComMod.cpp @@ -225,6 +225,5 @@ void sv1DSolverInterfaceType::set_data(const svOneDSolverInterfaceParameters& pa } solver_library = params.shared_library(); - input_file = params.input_file(); has_data = true; } diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index b64d10427..da89e70e5 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -204,6 +204,10 @@ class bcType // Coupled BC class CoupledBoundaryCondition coupled_bc; + + // sv1D: per-face 1D solver input file path (set when Time_dependence=Coupled + // and svOneDSolver_interface is active). + std::string oned_input_file; }; /// @brief Class storing data for B-Splines. @@ -771,6 +775,9 @@ class cplFaceType // RCR type BC rcrType RCR; + + // sv1D: path to the per-face 1D solver input file. + std::string oned_input_file; }; //---------------------------- @@ -819,11 +826,9 @@ class sv1DSolverInterfaceType // or with extension if the full path is provided). std::string solver_library; - // Path to the 1D solver .in input file. - std::string input_file; - // If the data has been set for the interface. Set to true after // the svOneDSolver_interface XML element has been parsed. + // Note: the per-face input files are stored in cplFaceType::oned_input_file. bool has_data = false; void set_data(const svOneDSolverInterfaceParameters& params); diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 138378fc5..16a6e9b49 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -466,8 +466,9 @@ void BoundaryConditionRCRParameters::set_values(tinyxml2::XMLElement* xml_elem) CouplingInterfaceParameters::CouplingInterfaceParameters() { bool required = false; - set_parameter("svZeroDSolver_block", "", !required, svzerod_solver_block); - set_parameter("Chamber_cap_surface", "", !required, chamber_cap_surface); + set_parameter("svZeroDSolver_block", "", !required, svzerod_solver_block); + set_parameter("Chamber_cap_surface", "", !required, chamber_cap_surface); + set_parameter("svOneDSolver_input_file","", !required, svoned_input_file); } void CouplingInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) @@ -1187,7 +1188,8 @@ svOneDSolverInterfaceParameters::svOneDSolverInterfaceParameters() set_parameter("Coupling_type", "", required, coupling_type); set_parameter("Shared_library", "", required, shared_library); - set_parameter("Input_file", "", required, input_file); + // Note: Input_file is no longer defined here. Each coupled face specifies + // its own 1D model input file via . } void svOneDSolverInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index c3a8650b3..0eb060477 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -688,7 +688,6 @@ class svZeroDSolverInterfaceParameters : public ParameterLists /// /// Implicit /// /path/to/libsvoned_interface -/// /path/to/solver.in /// /// \endcode /// @@ -700,7 +699,10 @@ class svZeroDSolverInterfaceParameters : public ParameterLists /// Shared_library: Path to the 1D interface shared library. The /// extension (.so or .dylib) may be omitted; it will be appended /// automatically based on the platform. -/// Input_file: Path to the svOneDSolver .in input file. +/// +/// Each coupled face specifies its own input file via +/// ... +/// inside the corresponding element. // class svOneDSolverInterfaceParameters : public ParameterLists { @@ -714,7 +716,6 @@ class svOneDSolverInterfaceParameters : public ParameterLists Parameter coupling_type; Parameter shared_library; - Parameter input_file; bool value_set = false; }; @@ -802,6 +803,9 @@ class CouplingInterfaceParameters : public ParameterLists Parameter svzerod_solver_block; Parameter chamber_cap_surface; + // Path to the svOneDSolver .in input file for this face (1D coupling). + Parameter svoned_input_file; + bool value_set = false; }; diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index 4edc2658f..7153a2c4f 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -123,6 +123,11 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) com_mod.cplBC.fa[i].name = com_mod.msh[iM].fa[iFa].name; com_mod.cplBC.fa[i].y = 0.0; + // Copy per-face 1D input file if present (sv1D coupling). + if (!bc.oned_input_file.empty()) { + com_mod.cplBC.fa[i].oned_input_file = bc.oned_input_file; + } + if (utils::btest(bc.bType, iBC_Dir)) { com_mod.cplBC.fa[i].bGrp = CplBCType::cplBC_Dir; diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 64a1fb059..1288e3ec9 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -264,10 +264,27 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } } else { - // Coupled BC to GenBC - if (ci_set) { - throw std::runtime_error( - "[read_bc] is only valid when is defined on the equation."); + // genBC / sv1D path: uses cplBC.fa mechanism. + const bool sv1d_iface = com_mod.cplBC.sv1d_solver_interface.has_data; + + if (sv1d_iface) { + // For sv1D: each coupled face must specify its own 1D input file via + // ... + if (!ci_set || !bc_params->coupling_interface.svoned_input_file.defined()) { + throw std::runtime_error( + std::string("[read_bc] With , each 1D-coupled face needs " + " with (Time_dependence Coupled) " + "on face '") + + face_name + "'."); + } + lBc.oned_input_file = bc_params->coupling_interface.svoned_input_file.value(); + + } else { + if (ci_set) { + throw std::runtime_error( + "[read_bc] is only valid when or " + " is defined on the equation."); + } } lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index 6b1864272..bb3a8e101 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -11,14 +11,26 @@ // ----------------- // NEU coupling (1D inlet driven by 3D outflow): // 3D → 1D : flow rate Q (as params[3..4]) -// 1D → 3D : pressure P (as cplBCvalue) +// 1D → 3D : pressure P (as cpl_value) // BC applied on 3D face : Neumann (pressure traction) // // DIR coupling (1D outlet driven by 3D pressure): // 3D → 1D : pressure P (as params[3..4]) -// 1D → 3D : flow rate Q (as cplBCvalue) +// 1D → 3D : flow rate Q (as cpl_value) // BC applied on 3D face : Dirichlet (velocity profile) // +// Parallelism model +// ----------------- +// Unlike the 0D solver (which is solved once on the master rank), each +// 1D model is INDEPENDENT and has its own input file. Multiple 1D models +// are therefore solved in parallel: +// +// - Collect all sv1D-coupled faces into a list indexed 0..N-1. +// - Assign face (model) k to MPI rank k % nProcs. +// - Each rank owns and solves only its assigned model(s). +// - After solving, results are shared via MPI_Bcast from every owner +// rank so that ALL ranks know ALL cplBC.fa[i].y values. +// // params array passed to run_1d_simulation_step_1d_: // params[0] = 2.0 (number of time points) // params[1] = t_old (time at start of step) @@ -40,51 +52,64 @@ #include "utils.h" #include "sv1D_interface/OneDSolverInterface.h" +#include "mpi.h" + namespace sv1D { // --------------------------------------------------------------------------- -// Module-level state (one 1D solver instance per simulation). +// Per-model state. Each entry corresponds to one sv1D-coupled face (one 1D +// model). Indexed by the sequential order in which faces were added to +// cplBC.fa (i.e., entry k corresponds to the k-th sv1D face, which has +// cplBCptr == face_ptr_list[k]). // --------------------------------------------------------------------------- -static OneDSolverInterface* oned_interface = nullptr; +struct OneDModelState { + // Interface object (null on ranks that do not own this model). + OneDSolverInterface* interface = nullptr; + + // Problem identifier returned by the 1D library. + int problem_id = 0; + + // Total DOF count (nodes × 2). + int system_size = 0; + + // "NEU" or "DIR". + std::string coupling_type; + + // Index in the solution vector that corresponds to the coupled BC DOF. + int coupled_dof = 0; + + // Authoritative solution vector (only valid on the owning rank). + std::vector solution; -// Problem identifier assigned by the 1D library during initialization. -static int oned_model_id = 0; + // Owning MPI rank for this model. + int owner_rank = 0; -// Total DOF count of the 1D solution vector (nodes * 2: flow + area/pressure). -static int oned_system_size = 0; + // Index into cplBC.fa[] that this model services. + int fa_ptr = -1; +}; -// "NEU" or "DIR" — determined from the face BC type at initialization time. -static std::string oned_coupling_type; +// --------------------------------------------------------------------------- +// Module-level state. +// --------------------------------------------------------------------------- -// Index within the 1D solution vector that corresponds to the coupled DOF. -static int oned_coupled_dof = 0; +// One entry per sv1D-coupled face, filled during init_sv1D(). +static std::vector oned_models; -// 1D solution vector maintained by the 3D solver. -// This is the authoritative snapshot used to restore the 1D state before -// each call to run_step(). -static std::vector oned_solution; +// Shared library handle (one per process, loaded once). +static OneDSolverInterface* shared_lib_instance = nullptr; -// Simulation time tracked for the 1D solver (advanced only on 'L' steps). +// Simulation time (advanced only on 'L' steps). static double sv1DTime = 0.0; // --------------------------------------------------------------------------- -// Helper: find the first BC that uses cplBC.fa (iBC_cpl) and sv1D. -// Returns {ptr, bGrp} or throws. +// Helper: resolve the shared-library path (.so / .dylib / as-is). // --------------------------------------------------------------------------- -static std::pair find_1d_face(const ComMod& com_mod) +static std::string resolve_lib_path(const std::string& lib_base) { - using namespace consts; - const int iEq = 0; - const auto& eq = com_mod.eq[iEq]; - for (int iBc = 0; iBc < eq.nBc; iBc++) { - const auto& bc = eq.bc[iBc]; - int ptr = bc.cplBCptr; - if (ptr == -1) continue; - if (!utils::btest(bc.bType, iBC_cpl)) continue; - return {ptr, com_mod.cplBC.fa[ptr].bGrp}; - } - throw std::runtime_error("[sv1D::find_1d_face] No 1D-coupled face found in eq[0]."); + if (std::ifstream(lib_base + ".so").good()) return lib_base + ".so"; + if (std::ifstream(lib_base + ".dylib").good()) return lib_base + ".dylib"; + return lib_base; // already has extension, or will fail at dlopen time } // --------------------------------------------------------------------------- @@ -97,82 +122,79 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) auto& cplBC = com_mod.cplBC; auto& solver_if = cplBC.sv1d_solver_interface; auto& cm = com_mod.cm; + const int nProcs = cm.nProcs; + const int myRank = cm.taskId; if (!solver_if.has_data) { throw std::runtime_error("[sv1D::init_sv1D] sv1D solver interface data is missing."); } - // Determine the coupling direction (NEU or DIR) from the BC type of the - // first coupled face. Only master needs this but we resolve it globally. - auto [ptr, bGrp] = find_1d_face(com_mod); - oned_coupling_type = (bGrp == CplBCType::cplBC_Neu) ? "NEU" : "DIR"; - - if (cm.mas(cm_mod)) { - // ----- Load shared library ----- - const std::string lib_base = solver_if.solver_library; - // Try .so first (Linux), then .dylib (macOS). - std::string lib_path; - { - std::ifstream f_so(lib_base + ".so"); - std::ifstream f_dy(lib_base + ".dylib"); - if (f_so.good()) { - lib_path = lib_base + ".so"; - } else if (f_dy.good()) { - lib_path = lib_base + ".dylib"; - } else { - // Try using the path as-is (already includes extension). - lib_path = lib_base; - } + // ----- Collect the list of sv1D-coupled faces ----- + // We iterate over eq[0]'s BCs and pick those with iBC_cpl AND a non-empty + // oned_input_file. Their cplBCptr values are the indices into cplBC.fa[]. + { + const int iEq = 0; + const auto& eq = com_mod.eq[iEq]; + for (int iBc = 0; iBc < eq.nBc; iBc++) { + const auto& bc = eq.bc[iBc]; + int ptr = bc.cplBCptr; + if (ptr == -1) continue; + if (!utils::btest(bc.bType, iBC_cpl)) continue; + if (cplBC.fa[ptr].oned_input_file.empty()) continue; + + OneDModelState st; + st.fa_ptr = ptr; + st.coupling_type = (cplBC.fa[ptr].bGrp == CplBCType::cplBC_Neu) ? "NEU" : "DIR"; + oned_models.push_back(std::move(st)); } + } - oned_interface = new OneDSolverInterface(); - oned_interface->load_library(lib_path); - - // ----- Initialize 1D solver ----- - int problem_id = 0; - int system_size = 0; - oned_interface->initialize(solver_if.input_file, problem_id, - system_size, oned_coupling_type); - oned_model_id = problem_id; - oned_system_size = system_size; - - // ----- Synchronize time step ----- - oned_interface->set_external_step_size(oned_model_id, com_mod.dt); - - // ----- Retrieve coupled DOF index ----- - oned_interface->extract_coupled_dof(oned_model_id, oned_coupled_dof, - oned_coupling_type); - - // ----- Copy initial 1D solution to local buffer ----- - oned_solution.resize(oned_system_size, 0.0); - oned_interface->return_solution(oned_model_id, oned_solution.data(), - oned_system_size); - - // ----- Set initial cplBC.fa[ptr].y ----- - // For NEU: initial BC value is the pressure at the coupled node. - // For DIR: initial BC value is the flow at the coupled node. - // The coupled DOF index is oned_coupled_dof (0-based) in the solution - // vector, where even indices = flow, odd indices = area or pressure - // depending on the 1D formulation. The shared library returns the - // correct cplBCvalue via run_step, so we initialise y = 0 here and let - // the first calc_sv1D call set the real value. - cplBC.fa[ptr].y = 0.0; + if (oned_models.empty()) { + throw std::runtime_error("[sv1D::init_sv1D] No sv1D-coupled faces with input files found."); } - // Broadcast system_size and coupling metadata to slave processes. - cm.bcast(cm_mod, &oned_system_size); - cm.bcast(cm_mod, &oned_coupled_dof); + // ----- Load shared library (once per process) ----- + const std::string lib_path = resolve_lib_path(solver_if.solver_library); + shared_lib_instance = new OneDSolverInterface(); + shared_lib_instance->load_library(lib_path); + + // ----- Assign ranks and initialize owned models ----- + const int nModels = static_cast(oned_models.size()); + for (int k = 0; k < nModels; k++) { + auto& st = oned_models[k]; + st.owner_rank = k % nProcs; + + if (myRank == st.owner_rank) { + // This rank owns model k: initialize it. + const std::string& input_file = cplBC.fa[st.fa_ptr].oned_input_file; + int problem_id = 0; + int system_size = 0; + + shared_lib_instance->initialize(input_file, problem_id, system_size, + st.coupling_type); + st.problem_id = problem_id; + st.system_size = system_size; + st.interface = shared_lib_instance; + + shared_lib_instance->set_external_step_size(problem_id, com_mod.dt); + shared_lib_instance->extract_coupled_dof(problem_id, st.coupled_dof, + st.coupling_type); + + st.solution.resize(system_size, 0.0); + shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); + + // Initial cplBC.fa y = 0; first calc_sv1D call sets the real value. + cplBC.fa[st.fa_ptr].y = 0.0; + } - // Slave processes also allocate the solution buffer (unused but kept for - // any future parallel extension). - if (cm.slv(cm_mod)) { - oned_solution.resize(oned_system_size, 0.0); + // Broadcast system_size and coupled_dof from owner to all ranks so that + // every rank knows the metadata (needed for consistent broadcast later). + MPI_Bcast(&st.system_size, 1, MPI_INT, st.owner_rank, cm.com()); + MPI_Bcast(&st.coupled_dof, 1, MPI_INT, st.owner_rank, cm.com()); } - // Run one 'D' step so that the initial resistance term bc.r is populated - // before the first Newton iteration (matches the svZeroD pattern). + // Run one 'D' step to populate the initial resistance term bc.r. if (cplBC.schm != CplBCType::cplBC_E) { - // Flowrate/pressure are 0 at t=0; calc_sv1D handles this gracefully. calc_sv1D(com_mod, cm_mod, 'D'); } } @@ -184,95 +206,70 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) { using namespace consts; - auto& cplBC = com_mod.cplBC; - auto& cm = com_mod.cm; + auto& cplBC = com_mod.cplBC; + auto& cm = com_mod.cm; + const int myRank = cm.taskId; - // Only the master process drives the 1D solver. - if (cm.mas(cm_mod)) { - const int iEq = 0; - auto& eq = com_mod.eq[iEq]; + const double t_old = sv1DTime; + const double t_new = sv1DTime + com_mod.dt; + const int nModels = static_cast(oned_models.size()); - double t_old = sv1DTime; - double t_new = sv1DTime + com_mod.dt; + // Each rank solves its own models and then broadcasts the result. + for (int k = 0; k < nModels; k++) { + auto& st = oned_models[k]; + int fa_ptr = st.fa_ptr; - for (int iBc = 0; iBc < eq.nBc; iBc++) { - auto& bc = eq.bc[iBc]; - int ptr = bc.cplBCptr; - if (ptr == -1) continue; - if (!utils::btest(bc.bType, iBC_cpl)) continue; + double cpl_value = 0.0; + if (myRank == st.owner_rank) { // Build params = [2, t_old, t_new, BC_val_old, BC_val_new] - // params[1..2] carry the time window; t_old is also passed as the - // standalone current_time argument required by the 1D library API. double params[5]; params[0] = 2.0; params[1] = t_old; params[2] = t_new; - if (cplBC.fa[ptr].bGrp == CplBCType::cplBC_Neu) { - // NEU: 3D provides flow Q → 1D returns pressure P. - params[3] = cplBC.fa[ptr].Qo; - params[4] = cplBC.fa[ptr].Qn; + if (cplBC.fa[fa_ptr].bGrp == CplBCType::cplBC_Neu) { + params[3] = cplBC.fa[fa_ptr].Qo; + params[4] = cplBC.fa[fa_ptr].Qn; } else { - // DIR: 3D provides pressure P → 1D returns flow Q. - params[3] = cplBC.fa[ptr].Po; - params[4] = cplBC.fa[ptr].Pn; + params[3] = cplBC.fa[fa_ptr].Po; + params[4] = cplBC.fa[fa_ptr].Pn; } - // Use a working copy of the solution so that perturbation steps - // ('D') do not corrupt the committed state in oned_solution. - std::vector work_sol = oned_solution; - oned_interface->update_solution(oned_model_id, work_sol.data(), - oned_system_size); - - // Save flag: write VTK output only on the final 'L' step. - int save_flag = (BCFlag == 'L') ? 1 : 0; + // Working copy of solution so that 'D' steps don't corrupt the + // committed state. + std::vector work_sol = st.solution; + st.interface->update_solution(st.problem_id, work_sol.data(), st.system_size); - double cpl_value = 0.0; - int error_code = 0; + int save_flag = (BCFlag == 'L') ? 1 : 0; + int error_code = 0; - oned_interface->run_step(oned_model_id, t_old, save_flag, - oned_coupling_type, params, - work_sol.data(), cpl_value, error_code); + st.interface->run_step(st.problem_id, t_old, save_flag, + st.coupling_type, params, + work_sol.data(), cpl_value, error_code); if (error_code != 0) { - throw std::runtime_error("[sv1D::calc_sv1D] 1D solver step failed with error code " + - std::to_string(error_code)); + throw std::runtime_error( + "[sv1D::calc_sv1D] 1D solver step for face '" + + cplBC.fa[fa_ptr].name + "' failed with error code " + + std::to_string(error_code)); } - // Store the returned BC value. - cplBC.fa[ptr].y = cpl_value; - - // On the final iteration only, commit the updated solution so that - // the next time step starts from the correct state. + // Commit the updated solution only on the final iteration. if (BCFlag == 'L') { - oned_solution = work_sol; + st.solution = work_sol; } } - // Advance the 1D simulation clock on the final iteration. - if (BCFlag == 'L') { - sv1DTime += com_mod.dt; - } + // Broadcast the BC value returned by this model from its owner to all + // ranks so that every rank can update cplBC.fa[fa_ptr].y. + MPI_Bcast(&cpl_value, 1, MPI_DOUBLE, st.owner_rank, cm.com()); + cplBC.fa[fa_ptr].y = cpl_value; } - // Broadcast updated cplBC.fa[i].y to all parallel processes. - if (!cm.seq()) { - Vector y(cplBC.nFa); - - if (cm.mas(cm_mod)) { - for (int i = 0; i < cplBC.nFa; i++) { - y(i) = cplBC.fa[i].y; - } - } - - cm.bcast(cm_mod, y); - - if (cm.slv(cm_mod)) { - for (int i = 0; i < cplBC.nFa; i++) { - cplBC.fa[i].y = y(i); - } - } + // Advance the simulation clock after the final iteration. + if (BCFlag == 'L') { + sv1DTime += com_mod.dt; } } From 2afd2231022c681ff7e779b49fbeb4285d4f75ed Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:29 -0700 Subject: [PATCH 04/51] =?UTF-8?q?Address=20code=20review:=20init=20sv1DTim?= =?UTF-8?q?e=20from=20com=5Fmod.time,=20rename=20nModels=E2=86=92nTotalMod?= =?UTF-8?q?els,=20improve=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Code/Source/solver/Parameters.cpp | 6 ++++-- Code/Source/solver/sv1D_subroutines.cpp | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 16a6e9b49..1e8e6e317 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -1188,8 +1188,10 @@ svOneDSolverInterfaceParameters::svOneDSolverInterfaceParameters() set_parameter("Coupling_type", "", required, coupling_type); set_parameter("Shared_library", "", required, shared_library); - // Note: Input_file is no longer defined here. Each coupled face specifies - // its own 1D model input file via . + // Unlike svZeroDSolver (which uses a single global Input_file for all faces), + // svOneDSolver requires each coupled face to supply its own input file. + // Per-face input files are specified via + // inside each element. } void svOneDSolverInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index bb3a8e101..f7c8b6831 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -129,6 +129,10 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) throw std::runtime_error("[sv1D::init_sv1D] sv1D solver interface data is missing."); } + // Initialize the 1D simulation clock from the 3D solver's current time so + // that restarts and non-zero start times are handled correctly. + sv1DTime = com_mod.time; + // ----- Collect the list of sv1D-coupled faces ----- // We iterate over eq[0]'s BCs and pick those with iBC_cpl AND a non-empty // oned_input_file. Their cplBCptr values are the indices into cplBC.fa[]. @@ -159,8 +163,8 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) shared_lib_instance->load_library(lib_path); // ----- Assign ranks and initialize owned models ----- - const int nModels = static_cast(oned_models.size()); - for (int k = 0; k < nModels; k++) { + const int nTotalModels = static_cast(oned_models.size()); + for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; st.owner_rank = k % nProcs; @@ -212,10 +216,10 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) const double t_old = sv1DTime; const double t_new = sv1DTime + com_mod.dt; - const int nModels = static_cast(oned_models.size()); + const int nTotalModels = static_cast(oned_models.size()); // Each rank solves its own models and then broadcasts the result. - for (int k = 0; k < nModels; k++) { + for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; int fa_ptr = st.fa_ptr; From 68561d0c452b24f58478d76470dbe4b34ef16a91 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:29 -0700 Subject: [PATCH 05/51] Fix calc_sv1D: split solve and broadcast into two phases for true MPI parallelism --- Code/Source/solver/sv1D_subroutines.cpp | 96 +++++++++++++------------ 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index f7c8b6831..2a26889e1 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -218,57 +218,65 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) const double t_new = sv1DTime + com_mod.dt; const int nTotalModels = static_cast(oned_models.size()); - // Each rank solves its own models and then broadcasts the result. + // ----- Phase 1: each rank runs its own models without blocking ----- + // All ranks proceed through this loop simultaneously, each executing only + // the models it owns. No MPI call here, so model k on rank A and model k+1 + // on rank B truly run at the same time. + std::vector cpl_values(nTotalModels, 0.0); + for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; int fa_ptr = st.fa_ptr; - double cpl_value = 0.0; + if (myRank != st.owner_rank) continue; - if (myRank == st.owner_rank) { - // Build params = [2, t_old, t_new, BC_val_old, BC_val_new] - double params[5]; - params[0] = 2.0; - params[1] = t_old; - params[2] = t_new; - - if (cplBC.fa[fa_ptr].bGrp == CplBCType::cplBC_Neu) { - params[3] = cplBC.fa[fa_ptr].Qo; - params[4] = cplBC.fa[fa_ptr].Qn; - } else { - params[3] = cplBC.fa[fa_ptr].Po; - params[4] = cplBC.fa[fa_ptr].Pn; - } - - // Working copy of solution so that 'D' steps don't corrupt the - // committed state. - std::vector work_sol = st.solution; - st.interface->update_solution(st.problem_id, work_sol.data(), st.system_size); - - int save_flag = (BCFlag == 'L') ? 1 : 0; - int error_code = 0; - - st.interface->run_step(st.problem_id, t_old, save_flag, - st.coupling_type, params, - work_sol.data(), cpl_value, error_code); - - if (error_code != 0) { - throw std::runtime_error( - "[sv1D::calc_sv1D] 1D solver step for face '" + - cplBC.fa[fa_ptr].name + "' failed with error code " + - std::to_string(error_code)); - } - - // Commit the updated solution only on the final iteration. - if (BCFlag == 'L') { - st.solution = work_sol; - } + // Build params = [2, t_old, t_new, BC_val_old, BC_val_new] + double params[5]; + params[0] = 2.0; + params[1] = t_old; + params[2] = t_new; + + if (cplBC.fa[fa_ptr].bGrp == CplBCType::cplBC_Neu) { + params[3] = cplBC.fa[fa_ptr].Qo; + params[4] = cplBC.fa[fa_ptr].Qn; + } else { + params[3] = cplBC.fa[fa_ptr].Po; + params[4] = cplBC.fa[fa_ptr].Pn; } - // Broadcast the BC value returned by this model from its owner to all - // ranks so that every rank can update cplBC.fa[fa_ptr].y. - MPI_Bcast(&cpl_value, 1, MPI_DOUBLE, st.owner_rank, cm.com()); - cplBC.fa[fa_ptr].y = cpl_value; + // Working copy of solution so that 'D' steps don't corrupt the + // committed state. + std::vector work_sol = st.solution; + st.interface->update_solution(st.problem_id, work_sol.data(), st.system_size); + + int save_flag = (BCFlag == 'L') ? 1 : 0; + int error_code = 0; + + st.interface->run_step(st.problem_id, t_old, save_flag, + st.coupling_type, params, + work_sol.data(), cpl_values[k], error_code); + + if (error_code != 0) { + throw std::runtime_error( + "[sv1D::calc_sv1D] 1D solver step for face '" + + cplBC.fa[fa_ptr].name + "' failed with error code " + + std::to_string(error_code)); + } + + // Commit the updated solution only on the final iteration. + if (BCFlag == 'L') { + st.solution = work_sol; + } + } + + // ----- Phase 2: broadcast all results and update cplBC.fa[].y ----- + // After every rank has finished solving its own models, gather the results. + // Each MPI_Bcast here is a cheap scalar transfer; the expensive 1D solver + // work has already been done concurrently in Phase 1. + for (int k = 0; k < nTotalModels; k++) { + auto& st = oned_models[k]; + MPI_Bcast(&cpl_values[k], 1, MPI_DOUBLE, st.owner_rank, cm.com()); + cplBC.fa[st.fa_ptr].y = cpl_values[k]; } // Advance the simulation clock after the final iteration. From abe0fa6178cea9af1dd6b013a1d89f01f8794f9b Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:30 -0700 Subject: [PATCH 06/51] Support mixed sv1D + RCR boundary conditions --- Code/Source/solver/ComMod.h | 3 ++ Code/Source/solver/baf_ini.cpp | 3 ++ Code/Source/solver/read_files.cpp | 13 ++++-- Code/Source/solver/set_bc.cpp | 67 ++++++++++++++++++++++--------- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index da89e70e5..50d3d2f66 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -778,6 +778,9 @@ class cplFaceType // sv1D: path to the per-face 1D solver input file. std::string oned_input_file; + + // Whether this face uses RCR (Windkessel) boundary condition. + bool isRCR = false; }; //---------------------------- diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index 7153a2c4f..d241adfe8 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -145,6 +145,9 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) com_mod.cplBC.fa[i].RCR.Rd = bc.RCR.Rd; com_mod.cplBC.fa[i].RCR.Pd = bc.RCR.Pd; com_mod.cplBC.fa[i].RCR.Xo = bc.RCR.Xo; + if (utils::btest(bc.bType, iBC_RCR)) { + com_mod.cplBC.fa[i].isRCR = true; + } } else { throw std::runtime_error("Not a compatible cplBC_type"); } diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 1288e3ec9..d11f4937b 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -335,7 +335,8 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, lBc.RCR.Pd = bc_params->rcr.distal_pressure.value(); lBc.RCR.Xo = bc_params->rcr.initial_pressure.value(); - if (com_mod.cplBC.schm != CplBCType::cplBC_NA || com_mod.cplBC.xo.size() != 0) { + if ((com_mod.cplBC.schm != CplBCType::cplBC_NA && !com_mod.cplBC.useSv1D) || + com_mod.cplBC.xo.size() != 0) { throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC."); } com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; @@ -1559,9 +1560,13 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) if (std::set{Equation_fluid,Equation_FSI,Equation_CMM}.count(lEq.phys) == 0) { throw std::runtime_error("RCR-type BC is allowed for fluid/CMM/FSI eq. only."); } - cplBC.schm = CplBCType::cplBC_SI; - if (lEq.useTLS) { - cplBC.schm = CplBCType::cplBC_E; + // Only set coupling scheme if not already configured by an external solver (e.g. sv1D). + // When sv1D and RCR coexist, sv1D owns the scheme; RCR uses the same scheme. + if (!cplBC.useSv1D) { + cplBC.schm = CplBCType::cplBC_SI; + if (lEq.useTLS) { + cplBC.schm = CplBCType::cplBC_E; + } } cplBC.nX = cplBC.nFa; cplBC.nXp = cplBC.nFa + 1; diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 67a9967a2..bf9a3242b 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -176,6 +176,10 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { sv1D::calc_sv1D(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with sv1D faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } @@ -226,6 +230,10 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { sv1D::calc_sv1D(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with sv1D faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } @@ -481,26 +489,40 @@ void RCR_Integ_X(ComMod& com_mod, const CmMod& cm_mod, int istat) double tt = fmax(time - dt, 0.0); double dtt = dt / static_cast(nTS); - int nX = cplBC.nFa; + + // Collect indices of RCR faces only. When sv1D and RCR are mixed, some + // cplBC.fa[] entries belong to the 1D solver and must be skipped here to + // avoid accessing uninitialised RCR parameters (Rp/C/Rd/Pd = 0) and + // causing division-by-zero inside the RK4 loop. + std::vector rcrIdx; + rcrIdx.reserve(cplBC.nFa); + for (int i = 0; i < cplBC.nFa; i++) { + if (cplBC.fa[i].isRCR) { + rcrIdx.push_back(i); + } + } + int nX = static_cast(rcrIdx.size()); Vector Rp(nX), C(nX), Rd(nX), Pd(nX); Vector X(nX), Xrk(nX); Array frk(nX,4), Qrk(nX,4); - for (int i = 0; i < nX; i++) { - Rp(i) = cplBC.fa[i].RCR.Rp; - C(i) = cplBC.fa[i].RCR.C; - Rd(i) = cplBC.fa[i].RCR.Rd; - Pd(i) = cplBC.fa[i].RCR.Pd; + for (int k = 0; k < nX; k++) { + int i = rcrIdx[k]; + Rp(k) = cplBC.fa[i].RCR.Rp; + C(k) = cplBC.fa[i].RCR.C; + Rd(k) = cplBC.fa[i].RCR.Rd; + Pd(k) = cplBC.fa[i].RCR.Pd; + X(k) = cplBC.xo[i]; } - X = cplBC.xo; for (int n = 0; n < nTS; n++) { for (int i = 0; i < 4; i++) { double r = static_cast(i) / 3.0; r = (static_cast(n) + r) / static_cast(nTS); - for (int j = 0; j < Qrk.nrows(); j++) { - Qrk(j,i) = cplBC.fa[j].Qo + (cplBC.fa[j].Qn - cplBC.fa[j].Qo) * r; + for (int j = 0; j < nX; j++) { + int fi = rcrIdx[j]; + Qrk(j,i) = cplBC.fa[fi].Qo + (cplBC.fa[fi].Qn - cplBC.fa[fi].Qo) * r; } } @@ -517,21 +539,21 @@ void RCR_Integ_X(ComMod& com_mod, const CmMod& cm_mod, int istat) trk = tt + dtt / 3.0; Xrk = X + dtt * frk.col(0) / 3.0; - for (int j = 0; j < Qrk.nrows(); j++) { + for (int j = 0; j < nX; j++) { frk(j,1) = (Qrk(j,1) - (Xrk(j)-Pd(j)) / Rd(j)) / C(j); } // RK-4 3rd pass trk = tt + 2.0 * dtt / 3.0; Xrk = X - dtt * frk.col(0) / 3.0 + dtt * frk.col(1); - for (int j = 0; j < Qrk.nrows(); j++) { + for (int j = 0; j < nX; j++) { frk(j,2) = (Qrk(j,2) - (Xrk(j) - Pd(j)) / Rd(j)) / C(j); } // RK-4 4th pass trk = tt + dtt; Xrk = X + dtt * frk.col(0) - dtt * frk.col(1) + dtt * frk.col(2); - for (int j = 0; j < Qrk.nrows(); j++) { + for (int j = 0; j < nX; j++) { frk(j,3) = (Qrk(j,3) - (Xrk(j) - Pd(j)) / Rd(j)) / C(j); } @@ -539,8 +561,8 @@ void RCR_Integ_X(ComMod& com_mod, const CmMod& cm_mod, int istat) X = X + r*(frk.col(0) + 3.0*(frk.col(1) + frk.col(2)) + frk.col(3)); tt = tt + dtt; - for (int i = 0; i < nX; i++) { - if (isnan(X(i))) { + for (int k = 0; k < nX; k++) { + if (isnan(X(k))) { throw std::runtime_error("ERROR: NaN detected in RCR integration"); istat = -1; return; @@ -548,13 +570,14 @@ void RCR_Integ_X(ComMod& com_mod, const CmMod& cm_mod, int istat) } } - cplBC.xn = X; - cplBC.xp(0) = tt; - - for (int i = 0; i < nX; i++) { - cplBC.xp(i+1) = Qrk(i,3); //cplBC.fa(i).Qn - cplBC.fa[i].y = X(i) + (cplBC.fa[i].Qn * Rp(i)); + // Write results back using the original (sparse) face indices. + for (int k = 0; k < nX; k++) { + int i = rcrIdx[k]; + cplBC.xn[i] = X(k); + cplBC.xp(i+1) = Qrk(k,3); + cplBC.fa[i].y = X(k) + (cplBC.fa[i].Qn * Rp(k)); } + cplBC.xp(0) = tt; } @@ -800,6 +823,10 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { sv1D::calc_sv1D(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with sv1D faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else { set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } From 4e778d54a945f4099189931cea2e4a80c6304ffd Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:31 -0700 Subject: [PATCH 07/51] Update error message to reflect RCR+sv1D is now allowed --- Code/Source/solver/read_files.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index d11f4937b..a5b71f347 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -337,7 +337,7 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, if ((com_mod.cplBC.schm != CplBCType::cplBC_NA && !com_mod.cplBC.useSv1D) || com_mod.cplBC.xo.size() != 0) { - throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC."); + throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC (except alongside sv1D)."); } com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; lBc.cplBCptr = com_mod.cplBC.nFa - 1; From 21daef5cc51902fd18d905fa134ab831292e5f1f Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:31 -0700 Subject: [PATCH 08/51] init_sv1D: separate init loop from metadata broadcast for true parallel initialization --- Code/Source/solver/sv1D_subroutines.cpp | 77 ++++++++++++++++--------- 1 file changed, 50 insertions(+), 27 deletions(-) diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index 2a26889e1..2eff9f38c 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -23,13 +23,27 @@ // ----------------- // Unlike the 0D solver (which is solved once on the master rank), each // 1D model is INDEPENDENT and has its own input file. Multiple 1D models -// are therefore solved in parallel: +// are therefore read, initialized, and solved in parallel: // -// - Collect all sv1D-coupled faces into a list indexed 0..N-1. -// - Assign face (model) k to MPI rank k % nProcs. -// - Each rank owns and solves only its assigned model(s). -// - After solving, results are shared via MPI_Bcast from every owner -// rank so that ALL ranks know ALL cplBC.fa[i].y values. +// Initialization (init_sv1D): +// Phase 1 – parallel init: +// - Collect all sv1D-coupled faces into a list indexed 0..N-1. +// - Assign face (model) k to MPI rank k % nProcs. +// - Each rank reads and initializes ONLY its owned model(s) with no +// MPI synchronization, so all ranks work simultaneously. +// Phase 2 – batch metadata exchange: +// - After every rank has finished initializing its own model(s), +// share system_size and coupled_dof via MPI_Bcast so that all +// ranks know the sizes needed for subsequent result broadcasts. +// +// Time-stepping (calc_sv1D): +// Phase 1 – parallel solve: +// - Each rank runs run_step() for its owned model(s) with no MPI +// calls, so model k on rank A and model k+1 on rank B truly run +// concurrently. +// Phase 2 – batch result exchange: +// - After every rank has finished solving, results are shared via +// MPI_Bcast so that ALL ranks know ALL cplBC.fa[i].y values. // // params array passed to run_1d_simulation_step_1d_: // params[0] = 2.0 (number of time points) @@ -162,37 +176,46 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) shared_lib_instance = new OneDSolverInterface(); shared_lib_instance->load_library(lib_path); - // ----- Assign ranks and initialize owned models ----- + // ----- Assign ranks and initialize owned models (Phase 1: parallel) ----- + // No MPI calls in this loop. All ranks proceed simultaneously, each + // reading and initializing only the model(s) it owns. Rank k owns model k + // (assigned via k % nProcs), so for N models and N ranks every rank handles + // exactly one model with no inter-rank synchronization. const int nTotalModels = static_cast(oned_models.size()); for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; st.owner_rank = k % nProcs; - if (myRank == st.owner_rank) { - // This rank owns model k: initialize it. - const std::string& input_file = cplBC.fa[st.fa_ptr].oned_input_file; - int problem_id = 0; - int system_size = 0; + if (myRank != st.owner_rank) continue; - shared_lib_instance->initialize(input_file, problem_id, system_size, - st.coupling_type); - st.problem_id = problem_id; - st.system_size = system_size; - st.interface = shared_lib_instance; + // This rank owns model k: read the input file and initialize. + const std::string& input_file = cplBC.fa[st.fa_ptr].oned_input_file; + int problem_id = 0; + int system_size = 0; - shared_lib_instance->set_external_step_size(problem_id, com_mod.dt); - shared_lib_instance->extract_coupled_dof(problem_id, st.coupled_dof, - st.coupling_type); + shared_lib_instance->initialize(input_file, problem_id, system_size, + st.coupling_type); + st.problem_id = problem_id; + st.system_size = system_size; + st.interface = shared_lib_instance; - st.solution.resize(system_size, 0.0); - shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); + shared_lib_instance->set_external_step_size(problem_id, com_mod.dt); + shared_lib_instance->extract_coupled_dof(problem_id, st.coupled_dof, + st.coupling_type); - // Initial cplBC.fa y = 0; first calc_sv1D call sets the real value. - cplBC.fa[st.fa_ptr].y = 0.0; - } + st.solution.resize(system_size, 0.0); + shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); - // Broadcast system_size and coupled_dof from owner to all ranks so that - // every rank knows the metadata (needed for consistent broadcast later). + // Initial cplBC.fa y = 0; first calc_sv1D call sets the real value. + cplBC.fa[st.fa_ptr].y = 0.0; + } + + // ----- Broadcast metadata for all models (Phase 2: batch exchange) ----- + // All initialization is complete. Now share system_size and coupled_dof + // from each owner so that every rank knows the sizes needed for consistent + // result broadcasts in calc_sv1D. + for (int k = 0; k < nTotalModels; k++) { + auto& st = oned_models[k]; MPI_Bcast(&st.system_size, 1, MPI_INT, st.owner_rank, cm.com()); MPI_Bcast(&st.coupled_dof, 1, MPI_INT, st.owner_rank, cm.com()); } From 05044bda24975ded0f2a7f36259448f39f8dd08c Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:32 -0700 Subject: [PATCH 09/51] sv1D: add nProcs >= nTotalModels guard in init_sv1D --- Code/Source/solver/sv1D_subroutines.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/sv1D_subroutines.cpp index 2eff9f38c..f6b69461f 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/sv1D_subroutines.cpp @@ -171,6 +171,20 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) throw std::runtime_error("[sv1D::init_sv1D] No sv1D-coupled faces with input files found."); } + // ----- Guard: require at least one MPI rank per 1D model ----- + // Each rank owns exactly one model (owner_rank = k % nProcs). If nProcs < N + // a single rank would own multiple models and call shared_lib_instance->initialize() + // more than once, corrupting the static problem-ID state inside the shared library. + const int nTotalModels = static_cast(oned_models.size()); + if (nProcs < nTotalModels) { + throw std::runtime_error( + "[sv1D::init_sv1D] Number of MPI processes (" + std::to_string(nProcs) + + ") is less than the number of sv1D-coupled faces (" + + std::to_string(nTotalModels) + + "). Please run with at least " + std::to_string(nTotalModels) + + " MPI processes."); + } + // ----- Load shared library (once per process) ----- const std::string lib_path = resolve_lib_path(solver_if.solver_library); shared_lib_instance = new OneDSolverInterface(); @@ -181,7 +195,6 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) // reading and initializing only the model(s) it owns. Rank k owns model k // (assigned via k % nProcs), so for N models and N ranks every rank handles // exactly one model with no inter-rank synchronization. - const int nTotalModels = static_cast(oned_models.size()); for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; st.owner_rank = k % nProcs; From d088f23d353864d49c701cf0e3116751cb0ee1a2 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:33 -0700 Subject: [PATCH 10/51] =?UTF-8?q?rename=20sv1D=20=E2=86=92=20svOneD:=20fil?= =?UTF-8?q?es,=20namespace,=20functions,=20class,=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Code/Source/solver/CMakeLists.txt | 4 +- Code/Source/solver/ComMod.cpp | 2 +- Code/Source/solver/ComMod.h | 12 ++-- Code/Source/solver/baf_ini.cpp | 6 +- Code/Source/solver/read_files.cpp | 10 ++-- Code/Source/solver/set_bc.cpp | 16 +++--- .../OneDSolverInterface.cpp | 0 .../OneDSolverInterface.h | 0 ...subroutines.cpp => svOneD_subroutines.cpp} | 56 +++++++++---------- ...v1D_subroutines.h => svOneD_subroutines.h} | 10 ++-- Code/Source/solver/txt.cpp | 4 +- 11 files changed, 60 insertions(+), 60 deletions(-) rename Code/Source/solver/{sv1D_interface => svOneD_interface}/OneDSolverInterface.cpp (100%) rename Code/Source/solver/{sv1D_interface => svOneD_interface}/OneDSolverInterface.h (100%) rename Code/Source/solver/{sv1D_subroutines.cpp => svOneD_subroutines.cpp} (88%) rename Code/Source/solver/{sv1D_subroutines.h => svOneD_subroutines.h} (76%) diff --git a/Code/Source/solver/CMakeLists.txt b/Code/Source/solver/CMakeLists.txt index 14863afea..05a3f1e9c 100644 --- a/Code/Source/solver/CMakeLists.txt +++ b/Code/Source/solver/CMakeLists.txt @@ -204,7 +204,7 @@ set(CSRCS stokes.h stokes.cpp sv_struct.h sv_struct.cpp svZeroD_interface.h svZeroD_interface.cpp - sv1D_subroutines.h sv1D_subroutines.cpp + svOneD_subroutines.h svOneD_subroutines.cpp txt.h txt.cpp utils.h utils.cpp ustruct.h ustruct.cpp @@ -229,7 +229,7 @@ set(CSRCS SPLIT.c svZeroD_interface/LPNSolverInterface.h svZeroD_interface/LPNSolverInterface.cpp - sv1D_interface/OneDSolverInterface.h sv1D_interface/OneDSolverInterface.cpp + svOneD_interface/OneDSolverInterface.h svOneD_interface/OneDSolverInterface.cpp BoundaryCondition.h BoundaryCondition.cpp RobinBoundaryCondition.h RobinBoundaryCondition.cpp diff --git a/Code/Source/solver/ComMod.cpp b/Code/Source/solver/ComMod.cpp index d9b0b676c..7d543dd15 100644 --- a/Code/Source/solver/ComMod.cpp +++ b/Code/Source/solver/ComMod.cpp @@ -218,7 +218,7 @@ void svZeroDSolverInterfaceType::set_data(const svZeroDSolverInterfaceParameters has_data = true; } -void sv1DSolverInterfaceType::set_data(const svOneDSolverInterfaceParameters& params) +void svOneDSolverInterfaceType::set_data(const svOneDSolverInterfaceParameters& params) { if (!params.defined()) { return; diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index 50d3d2f66..32a18b3af 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -205,7 +205,7 @@ class bcType // Coupled BC class CoupledBoundaryCondition coupled_bc; - // sv1D: per-face 1D solver input file path (set when Time_dependence=Coupled + // svOneD: per-face 1D solver input file path (set when Time_dependence=Coupled // and svOneDSolver_interface is active). std::string oned_input_file; }; @@ -776,7 +776,7 @@ class cplFaceType // RCR type BC rcrType RCR; - // sv1D: path to the per-face 1D solver input file. + // svOneD: path to the per-face 1D solver input file. std::string oned_input_file; // Whether this face uses RCR (Windkessel) boundary condition. @@ -818,11 +818,11 @@ class svZeroDSolverInterfaceType }; //---------------------------- -// sv1DSolverInterfaceType +// svOneDSolverInterfaceType //---------------------------- // This class stores information used to interface to the svOneDSolver. // -class sv1DSolverInterfaceType +class svOneDSolverInterfaceType { public: // Path to the 1D solver shared library (without .so/.dylib extension, @@ -852,7 +852,7 @@ class cplBCType // Whether to use svZeroD bool useSvZeroD = false; - // Whether to use sv1D (svOneDSolver) + // Whether to use svOneD (svOneDSolver) bool useSv1D = false; // Whether to initialize RCR from flow data @@ -886,7 +886,7 @@ class cplBCType svZeroDSolverInterfaceType svzerod_solver_interface; - sv1DSolverInterfaceType sv1d_solver_interface; + svOneDSolverInterfaceType sv1d_solver_interface; /// @brief The name of history file containing "X" std::string saveName; diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index d241adfe8..4f18f177f 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -12,7 +12,7 @@ #include "set_bc.h" #include "utils.h" #include "svZeroD_interface.h" -#include "sv1D_subroutines.h" +#include "svOneD_subroutines.h" #include "fsils_api.hpp" #include "fils_struct.hpp" @@ -123,7 +123,7 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) com_mod.cplBC.fa[i].name = com_mod.msh[iM].fa[iFa].name; com_mod.cplBC.fa[i].y = 0.0; - // Copy per-face 1D input file if present (sv1D coupling). + // Copy per-face 1D input file if present (svOneD coupling). if (!bc.oned_input_file.empty()) { com_mod.cplBC.fa[i].oned_input_file = bc.oned_input_file; } @@ -172,7 +172,7 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) } if (com_mod.cplBC.useSv1D) { - sv1D::init_sv1D(com_mod, cm_mod); + svOneD::init_svOneD(com_mod, cm_mod); } // Initialize cap integration for Coupled boundary conditions diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index a5b71f347..4461d0849 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -264,11 +264,11 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } } else { - // genBC / sv1D path: uses cplBC.fa mechanism. + // genBC / cplBC / svOneD path: uses cplBC.fa mechanism. const bool sv1d_iface = com_mod.cplBC.sv1d_solver_interface.has_data; if (sv1d_iface) { - // For sv1D: each coupled face must specify its own 1D input file via + // For svOneD: each coupled face must specify its own 1D input file via // ... if (!ci_set || !bc_params->coupling_interface.svoned_input_file.defined()) { throw std::runtime_error( @@ -337,7 +337,7 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, if ((com_mod.cplBC.schm != CplBCType::cplBC_NA && !com_mod.cplBC.useSv1D) || com_mod.cplBC.xo.size() != 0) { - throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC (except alongside sv1D)."); + throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC (except alongside svOneD)."); } com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; lBc.cplBCptr = com_mod.cplBC.nFa - 1; @@ -1560,8 +1560,8 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) if (std::set{Equation_fluid,Equation_FSI,Equation_CMM}.count(lEq.phys) == 0) { throw std::runtime_error("RCR-type BC is allowed for fluid/CMM/FSI eq. only."); } - // Only set coupling scheme if not already configured by an external solver (e.g. sv1D). - // When sv1D and RCR coexist, sv1D owns the scheme; RCR uses the same scheme. + // Only set coupling scheme if not already configured by an external solver (e.g. svOneD). + // When svOneD and RCR coexist, svOneD owns the scheme; RCR uses the same scheme. if (!cplBC.useSv1D) { cplBC.schm = CplBCType::cplBC_SI; if (lEq.useTLS) { diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index bf9a3242b..a0006f8d6 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -18,7 +18,7 @@ #include "utils.h" #include #include "svZeroD_interface.h" -#include "sv1D_subroutines.h" +#include "svOneD_subroutines.h" namespace set_bc { @@ -175,8 +175,8 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { - sv1D::calc_sv1D(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with sv1D faces. + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svOneD faces. if (RCRflag) { set_bc::cplBC_Integ_X(com_mod, cm_mod, true); } @@ -229,8 +229,8 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { - sv1D::calc_sv1D(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with sv1D faces. + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svOneD faces. if (RCRflag) { set_bc::cplBC_Integ_X(com_mod, cm_mod, true); } @@ -490,7 +490,7 @@ void RCR_Integ_X(ComMod& com_mod, const CmMod& cm_mod, int istat) double tt = fmax(time - dt, 0.0); double dtt = dt / static_cast(nTS); - // Collect indices of RCR faces only. When sv1D and RCR are mixed, some + // Collect indices of RCR faces only. When svOneD and RCR are mixed, some // cplBC.fa[] entries belong to the 1D solver and must be skipped here to // avoid accessing uninitialised RCR parameters (Rp/C/Rd/Pd = 0) and // causing division-by-zero inside the RK4 loop. @@ -822,8 +822,8 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) } else if (cplBC.useSvZeroD){ svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } else if (cplBC.useSv1D) { - sv1D::calc_sv1D(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with sv1D faces. + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svOneD faces. if (RCRflag) { set_bc::cplBC_Integ_X(com_mod, cm_mod, true); } diff --git a/Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp b/Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp similarity index 100% rename from Code/Source/solver/sv1D_interface/OneDSolverInterface.cpp rename to Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp diff --git a/Code/Source/solver/sv1D_interface/OneDSolverInterface.h b/Code/Source/solver/svOneD_interface/OneDSolverInterface.h similarity index 100% rename from Code/Source/solver/sv1D_interface/OneDSolverInterface.h rename to Code/Source/solver/svOneD_interface/OneDSolverInterface.h diff --git a/Code/Source/solver/sv1D_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp similarity index 88% rename from Code/Source/solver/sv1D_subroutines.cpp rename to Code/Source/solver/svOneD_subroutines.cpp index f6b69461f..3d9b8f85e 100644 --- a/Code/Source/solver/sv1D_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -25,9 +25,9 @@ // 1D model is INDEPENDENT and has its own input file. Multiple 1D models // are therefore read, initialized, and solved in parallel: // -// Initialization (init_sv1D): +// Initialization (init_svOneD): // Phase 1 – parallel init: -// - Collect all sv1D-coupled faces into a list indexed 0..N-1. +// - Collect all svOneD-coupled faces into a list indexed 0..N-1. // - Assign face (model) k to MPI rank k % nProcs. // - Each rank reads and initializes ONLY its owned model(s) with no // MPI synchronization, so all ranks work simultaneously. @@ -36,7 +36,7 @@ // share system_size and coupled_dof via MPI_Bcast so that all // ranks know the sizes needed for subsequent result broadcasts. // -// Time-stepping (calc_sv1D): +// Time-stepping (calc_svOneD): // Phase 1 – parallel solve: // - Each rank runs run_step() for its owned model(s) with no MPI // calls, so model k on rank A and model k+1 on rank B truly run @@ -52,7 +52,7 @@ // params[3] = BC_val_old (Q or P at t_old) // params[4] = BC_val_new (Q or P at t_new) -#include "sv1D_subroutines.h" +#include "svOneD_subroutines.h" #include #include @@ -64,16 +64,16 @@ #include "ComMod.h" #include "consts.h" #include "utils.h" -#include "sv1D_interface/OneDSolverInterface.h" +#include "svOneD_interface/OneDSolverInterface.h" #include "mpi.h" -namespace sv1D { +namespace svOneD { // --------------------------------------------------------------------------- -// Per-model state. Each entry corresponds to one sv1D-coupled face (one 1D +// Per-model state. Each entry corresponds to one svOneD-coupled face (one 1D // model). Indexed by the sequential order in which faces were added to -// cplBC.fa (i.e., entry k corresponds to the k-th sv1D face, which has +// cplBC.fa (i.e., entry k corresponds to the k-th svOneD face, which has // cplBCptr == face_ptr_list[k]). // --------------------------------------------------------------------------- @@ -107,14 +107,14 @@ struct OneDModelState { // Module-level state. // --------------------------------------------------------------------------- -// One entry per sv1D-coupled face, filled during init_sv1D(). +// One entry per svOneD-coupled face, filled during init_svOneD(). static std::vector oned_models; // Shared library handle (one per process, loaded once). static OneDSolverInterface* shared_lib_instance = nullptr; // Simulation time (advanced only on 'L' steps). -static double sv1DTime = 0.0; +static double svOneDTime = 0.0; // --------------------------------------------------------------------------- // Helper: resolve the shared-library path (.so / .dylib / as-is). @@ -127,9 +127,9 @@ static std::string resolve_lib_path(const std::string& lib_base) } // --------------------------------------------------------------------------- -// init_sv1D +// init_svOneD // --------------------------------------------------------------------------- -void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) +void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) { using namespace consts; @@ -140,14 +140,14 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) const int myRank = cm.taskId; if (!solver_if.has_data) { - throw std::runtime_error("[sv1D::init_sv1D] sv1D solver interface data is missing."); + throw std::runtime_error("[svOneD::init_svOneD] svOneD solver interface data is missing."); } // Initialize the 1D simulation clock from the 3D solver's current time so // that restarts and non-zero start times are handled correctly. - sv1DTime = com_mod.time; + svOneDTime = com_mod.time; - // ----- Collect the list of sv1D-coupled faces ----- + // ----- Collect the list of svOneD-coupled faces ----- // We iterate over eq[0]'s BCs and pick those with iBC_cpl AND a non-empty // oned_input_file. Their cplBCptr values are the indices into cplBC.fa[]. { @@ -168,7 +168,7 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) } if (oned_models.empty()) { - throw std::runtime_error("[sv1D::init_sv1D] No sv1D-coupled faces with input files found."); + throw std::runtime_error("[svOneD::init_svOneD] No svOneD-coupled faces with input files found."); } // ----- Guard: require at least one MPI rank per 1D model ----- @@ -178,8 +178,8 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) const int nTotalModels = static_cast(oned_models.size()); if (nProcs < nTotalModels) { throw std::runtime_error( - "[sv1D::init_sv1D] Number of MPI processes (" + std::to_string(nProcs) + - ") is less than the number of sv1D-coupled faces (" + + "[svOneD::init_svOneD] Number of MPI processes (" + std::to_string(nProcs) + + ") is less than the number of svOneD-coupled faces (" + std::to_string(nTotalModels) + "). Please run with at least " + std::to_string(nTotalModels) + " MPI processes."); @@ -219,14 +219,14 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) st.solution.resize(system_size, 0.0); shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); - // Initial cplBC.fa y = 0; first calc_sv1D call sets the real value. + // Initial cplBC.fa y = 0; first calc_svOneD call sets the real value. cplBC.fa[st.fa_ptr].y = 0.0; } // ----- Broadcast metadata for all models (Phase 2: batch exchange) ----- // All initialization is complete. Now share system_size and coupled_dof // from each owner so that every rank knows the sizes needed for consistent - // result broadcasts in calc_sv1D. + // result broadcasts in calc_svOneD. for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; MPI_Bcast(&st.system_size, 1, MPI_INT, st.owner_rank, cm.com()); @@ -235,14 +235,14 @@ void init_sv1D(ComMod& com_mod, const CmMod& cm_mod) // Run one 'D' step to populate the initial resistance term bc.r. if (cplBC.schm != CplBCType::cplBC_E) { - calc_sv1D(com_mod, cm_mod, 'D'); + calc_svOneD(com_mod, cm_mod, 'D'); } } // --------------------------------------------------------------------------- -// calc_sv1D +// calc_svOneD // --------------------------------------------------------------------------- -void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) +void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) { using namespace consts; @@ -250,8 +250,8 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) auto& cm = com_mod.cm; const int myRank = cm.taskId; - const double t_old = sv1DTime; - const double t_new = sv1DTime + com_mod.dt; + const double t_old = svOneDTime; + const double t_new = svOneDTime + com_mod.dt; const int nTotalModels = static_cast(oned_models.size()); // ----- Phase 1: each rank runs its own models without blocking ----- @@ -294,7 +294,7 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (error_code != 0) { throw std::runtime_error( - "[sv1D::calc_sv1D] 1D solver step for face '" + + "[svOneD::calc_svOneD] 1D solver step for face '" + cplBC.fa[fa_ptr].name + "' failed with error code " + std::to_string(error_code)); } @@ -317,8 +317,8 @@ void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Advance the simulation clock after the final iteration. if (BCFlag == 'L') { - sv1DTime += com_mod.dt; + svOneDTime += com_mod.dt; } } -} // namespace sv1D +} // namespace svOneD diff --git a/Code/Source/solver/sv1D_subroutines.h b/Code/Source/solver/svOneD_subroutines.h similarity index 76% rename from Code/Source/solver/sv1D_subroutines.h rename to Code/Source/solver/svOneD_subroutines.h index e368dcfb5..6b8009cd0 100644 --- a/Code/Source/solver/sv1D_subroutines.h +++ b/Code/Source/solver/svOneD_subroutines.h @@ -6,20 +6,20 @@ #include "Simulation.h" #include "consts.h" -#include "sv1D_interface/OneDSolverInterface.h" +#include "svOneD_interface/OneDSolverInterface.h" -namespace sv1D { +namespace svOneD { /// @brief Initialize the 1D solver and populate the initial cplBC state. /// Called once from baf_ini() after the BC data structures are set up. -void init_sv1D(ComMod& com_mod, const CmMod& cm_mod); +void init_svOneD(ComMod& com_mod, const CmMod& cm_mod); /// @brief Advance the 1D solver by one time step and update the coupled BC value. /// /// @param BCFlag 'D' - derivative / perturbation step (state is NOT committed). /// 'L' - last Newton iteration (state IS committed, time advances). -void calc_sv1D(ComMod& com_mod, const CmMod& cm_mod, char BCFlag); +void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag); -} // namespace sv1D +} // namespace svOneD #endif // SV1D_SUBROUTINES_H diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index c9d56fce2..9d02be893 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -13,7 +13,7 @@ #include "utils.h" #include #include "svZeroD_interface.h" -#include "sv1D_subroutines.h" +#include "svOneD_subroutines.h" namespace txt_ns { @@ -179,7 +179,7 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so svZeroD::calc_svZeroD(com_mod, cm_mod, 'L'); } else if (cplBC.useSv1D) { - sv1D::calc_sv1D(com_mod, cm_mod, 'L'); + svOneD::calc_svOneD(com_mod, cm_mod, 'L'); } else { for (auto& bc : com_mod.eq[0].bc) { From 1127a717c2d895c2bdb88440877d73e4c28133c0 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:34 -0700 Subject: [PATCH 11/51] Add example solver.xml with 2 svOneD-coupled faces --- .../cases/fluid/pipe_svOneD_2faces/solver.xml | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 tests/cases/fluid/pipe_svOneD_2faces/solver.xml diff --git a/tests/cases/fluid/pipe_svOneD_2faces/solver.xml b/tests/cases/fluid/pipe_svOneD_2faces/solver.xml new file mode 100644 index 000000000..bc15d6bad --- /dev/null +++ b/tests/cases/fluid/pipe_svOneD_2faces/solver.xml @@ -0,0 +1,142 @@ + + + + + + + false + 3 + 100 + 0.001 + 0.50 + STOP_SIM + + 1 + result + 10 + 1 + + 100 + 0 + + 1 + 0 + 0 + + + + + + mesh-complete/mesh-complete.mesh.vtu + + + mesh-complete/mesh-surfaces/lumen_inlet.vtp + + + + mesh-complete/mesh-surfaces/lumen_outlet1.vtp + + + + mesh-complete/mesh-surfaces/lumen_outlet2.vtp + + + + mesh-complete/mesh-surfaces/lumen_wall.vtp + + + + + + 1 + 3 + 10 + 1e-3 + 0.2 + + 1.06 + + 0.04 + + + + true + true + true + true + + + + + fsils + + 10 + 3 + 500 + 1e-3 + 1e-3 + 1e-3 + 50 + + + + + semi-implicit + /path/to/svOneDSolver/build/lib/libsvOneDSolver_interface.so + + + + + Dir + Unsteady + lumen_inlet.flw + true + true + + + + + Neu + Coupled + + 1dmodel1.in + + + + + + Neu + Coupled + + 1dmodel2.in + + + + + + Dir + Steady + 0.0 + + + + + From d88edb12d206399c0a20c4dd1e5b50112e7bed16 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:34 -0700 Subject: [PATCH 12/51] Migrate svOneD 1D coupling to CoupledBoundaryCondition interface --- .../solver/CoupledBoundaryCondition.cpp | 20 +- Code/Source/solver/CoupledBoundaryCondition.h | 9 + Code/Source/solver/read_files.cpp | 174 ++++++++++++------ Code/Source/solver/set_bc.cpp | 28 ++- Code/Source/solver/svOneD_subroutines.cpp | 58 +++--- 5 files changed, 192 insertions(+), 97 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index bf4ee9061..7c4f035b8 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -22,6 +22,7 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryConditio , bc_type_(other.bc_type_) , block_name_(other.block_name_) , face_name_(other.face_name_) + , oned_input_file_(other.oned_input_file_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -53,7 +54,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound bc_type_ = other.bc_type_; block_name_ = other.block_name_; face_name_ = other.face_name_; - Qo_ = other.Qo_; + oned_input_file_ = other.oned_input_file_; Qn_ = other.Qn_; Po_ = other.Po_; Pn_ = other.Pn_; @@ -83,7 +84,7 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , bc_type_(other.bc_type_) , block_name_(std::move(other.block_name_)) , face_name_(std::move(other.face_name_)) - , Qo_(other.Qo_) + , oned_input_file_(std::move(other.oned_input_file_)) , Qn_(other.Qn_) , Po_(other.Po_) , Pn_(other.Pn_) @@ -129,7 +130,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon bc_type_ = other.bc_type_; block_name_ = std::move(other.block_name_); face_name_ = std::move(other.face_name_); - Qo_ = other.Qo_; + oned_input_file_ = std::move(other.oned_input_file_); Qn_ = other.Qn_; Po_ = other.Po_; Pn_ = other.Pn_; @@ -208,6 +209,16 @@ const std::string& CoupledBoundaryCondition::get_block_name() const return block_name_; } +const std::string& CoupledBoundaryCondition::get_oned_input_file() const +{ + return oned_input_file_; +} + +void CoupledBoundaryCondition::set_oned_input_file(const std::string& path) +{ + oned_input_file_ = path; +} + void CoupledBoundaryCondition::set_solution_ids(int flow_id, int pressure_id, double in_out_sign) { flow_sol_id_ = flow_id; @@ -401,6 +412,9 @@ void CoupledBoundaryCondition::distribute(const ComMod& com_mod, const CmMod& cm // Distribute block name cm.bcast(cm_mod, block_name_); + // Distribute 1D input file path + cm.bcast(cm_mod, oned_input_file_); + // Distribute face name cm.bcast(cm_mod, face_name_); diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index ae6d54a6d..4e376a41a 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -191,6 +191,8 @@ class CoupledBoundaryCondition { /// @brief svZeroD coupling data std::string block_name_; ///< Block name in svZeroDSolver configuration std::string face_name_; ///< Face name from the mesh + /// @brief svOneD coupling data + std::string oned_input_file_; ///< Path to svOneDSolver input file (empty for svZeroD BCs) /// @brief Flowrate data double Qo_ = 0.0; ///< Flowrate at old timestep (t_n) @@ -289,6 +291,13 @@ class CoupledBoundaryCondition { /// @return Block name const std::string& get_block_name() const; + /// @brief Get the svOneD input file path + /// @return Path to the 1D solver input file (empty for svZeroD BCs) + const std::string& get_oned_input_file() const; + + /// @brief Set the svOneD input file path + void set_oned_input_file(const std::string& path); + /// @brief Set the svZeroD solution IDs for flow and pressure /// @param flow_id Flow solution ID /// @param pressure_id Pressure solution ID diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 4461d0849..8e30ef82d 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -264,10 +264,10 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } } else { - // genBC / cplBC / svOneD path: uses cplBC.fa mechanism. + // genBC / cplBC / svOneD path. const bool sv1d_iface = com_mod.cplBC.sv1d_solver_interface.has_data; - if (sv1d_iface) { + if (sv1d_iface) { // For svOneD: each coupled face must specify its own 1D input file via // ... if (!ci_set || !bc_params->coupling_interface.svoned_input_file.defined()) { @@ -279,23 +279,39 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } lBc.oned_input_file = bc_params->coupling_interface.svoned_input_file.value(); + // svOneD now uses the CoupledBoundaryCondition interface (same flags as svZeroD). + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_Dir)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_Neu)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_bfs)); + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); + // cplBCptr stays -1; coupled_bc is constructed in the block below. + + if (com_mod.cplBC.schm == CplBCType::cplBC_NA) { + throw std::runtime_error( + std::string("[read_bc] A coupling method (e.g. svOneDSolver_interface coupling_type) " + "must be defined for Time_dependence Coupled on face '") + + face_name + "'."); + } + } else { + // genBC / cplBC: uses cplBC.fa array mechanism. if (ci_set) { throw std::runtime_error( "[read_bc] is only valid when or " " is defined on the equation."); } - } - lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); - com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; - lBc.cplBCptr = com_mod.cplBC.nFa - 1; + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); + com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; + lBc.cplBCptr = com_mod.cplBC.nFa - 1; - if (com_mod.cplBC.schm == CplBCType::cplBC_NA) { - throw std::runtime_error( - std::string("[read_bc] A coupling method (e.g. Couple_to_genBC) must be defined for Time_dependence " - "Coupled on face '") + - face_name + "'."); + if (com_mod.cplBC.schm == CplBCType::cplBC_NA) { + throw std::runtime_error( + std::string("[read_bc] A coupling method (e.g. Couple_to_genBC) must be defined for Time_dependence " + "Coupled on face '") + + face_name + "'."); + } } } @@ -412,67 +428,105 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } - // Coupled BC (svZeroDSolver / CoupledBoundaryCondition) + // Coupled BC (svZeroDSolver or svOneDSolver via CoupledBoundaryCondition) if (utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled))) { - // Get asssociated face name and block name + // Get associated face name const auto& face_name = com_mod.msh[lBc.iM].fa[lBc.iFa].name; - const std::string zd_block = bc_params->coupling_interface.svzerod_solver_block.value(); - - // Get follower pressure load flag if defined - bool cpl_flwP = false; - if (lEq.phys == Equation_struct || lEq.phys == Equation_ustruct) { - cpl_flwP = bc_params->follower_pressure_load.value(); - } - // Sanity check: CoupledBoundaryCondition is only supported for struct, ustruct, fluid, FSI, or CMM physics - const auto cpl_phys = lEq.phys; - if (cpl_phys != Equation_struct && cpl_phys != Equation_ustruct && cpl_phys != Equation_fluid && - cpl_phys != Equation_FSI && cpl_phys != Equation_CMM) { + // Sanity check: CoupledBoundaryCondition must be Dirichlet or Neumann + if (coupled_bc_type != BoundaryConditionType::bType_Dir && + coupled_bc_type != BoundaryConditionType::bType_Neu) { throw std::runtime_error( - std::string("[read_bc] CoupledBoundaryCondition (svZeroDSolver) is only supported for struct, ustruct, fluid, FSI, or CMM physics on face '") + - face_name + "'."); - } - - // Sanity check: svZeroDSolver coupling is currently implemented only for Neumann-type boundaries. - if (!utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Neu))) { - throw std::runtime_error( - std::string("[read_bc] CoupledBoundaryCondition (svZeroDSolver) currently requires boundary Neu on face '") + + std::string("[read_bc] CoupledBoundaryCondition requires boundary Dirichlet or Neumann on face '") + face_name + "'."); } - // Sanity check: Follower pressure load must be used for 0D coupling with struct/ustruct - if ((cpl_phys == Equation_struct || cpl_phys == Equation_ustruct) && !cpl_flwP) { - throw std::runtime_error( - std::string("[read_bc] Follower pressure load must be used for 0D coupling with struct/ustruct on face '") + - face_name + "'."); - } - - // Get cap face VTP file name if defined - std::string zerod_cap; - bool use_cap = false; - if (bc_params->coupling_interface.chamber_cap_surface.defined()) { - zerod_cap = bc_params->coupling_interface.chamber_cap_surface.value(); - use_cap = true; - } + if (com_mod.cplBC.sv1d_solver_interface.has_data) { + // ------------------------------------------------------------------ + // svOneD path: construct CoupledBoundaryCondition with empty + // block_name and store the 1D input file path. + // ------------------------------------------------------------------ - // Figure out the coupled BC type - BoundaryConditionType coupled_bc_type = BoundaryConditionType::bType_Neu; - if (utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Dir))) { - coupled_bc_type = BoundaryConditionType::bType_Dir; - } else if (utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Neu))) { - coupled_bc_type = BoundaryConditionType::bType_Neu; - } + // Sanity check: only fluid, FSI, or CMM physics supported + const auto cpl_phys = lEq.phys; + if (cpl_phys != Equation_fluid && cpl_phys != Equation_FSI && cpl_phys != Equation_CMM) { + throw std::runtime_error( + std::string("[read_bc] CoupledBoundaryCondition (svOneDSolver) is only supported for " + "fluid, FSI, or CMM physics on face '") + + face_name + "'."); + } - // Create the coupled boundary condition object - if (use_cap) { lBc.coupled_bc = CoupledBoundaryCondition(coupled_bc_type, com_mod.msh[lBc.iM].fa[lBc.iFa], - com_mod.msh[lBc.iM].fa[lBc.iFa].name, zd_block, zerod_cap, - lEq.phys, cpl_flwP); + com_mod.msh[lBc.iM].fa[lBc.iFa].name, + /*block_name=*/"", lEq.phys, /*follower_pressure_load=*/false); + lBc.coupled_bc.set_oned_input_file(lBc.oned_input_file); + + } else if (com_mod.cplBC.svzerod_solver_interface.has_data) { + // ------------------------------------------------------------------ + // svZeroD path (existing). + // ------------------------------------------------------------------ + + // Sanity check: must be defined + if (!bc_params->coupling_interface.value_set) { + throw std::runtime_error( + std::string("[read_bc] CoupledBoundaryCondition requires for face '") + face_name + + "'."); + } + + // Sanity check: must be defined + if (!bc_params->coupling_interface.svzerod_solver_block.defined()) { + throw std::runtime_error( + std::string("[read_bc] must define for face '") + face_name + + "'."); + } + + // Get block name + const std::string zd_block = bc_params->coupling_interface.svzerod_solver_block.value(); + + // Get follower pressure load flag if defined + bool cpl_flwP = false; + if (lEq.phys == Equation_struct || lEq.phys == Equation_ustruct) { + cpl_flwP = bc_params->follower_pressure_load.value(); + } + + // Sanity check: CoupledBoundaryCondition is only supported for struct, ustruct, fluid, FSI, or CMM physics + const auto cpl_phys = lEq.phys; + if (cpl_phys != Equation_struct && cpl_phys != Equation_ustruct && cpl_phys != Equation_fluid && + cpl_phys != Equation_FSI && cpl_phys != Equation_CMM) { + throw std::runtime_error( + std::string("[read_bc] CoupledBoundaryCondition (svZeroDSolver) is only supported for struct, ustruct, fluid, FSI, or CMM physics on face '") + + face_name + "'."); + } + + // Sanity check: Follower pressure load must be used for 0D coupling with struct/ustruct + if ((cpl_phys == Equation_struct || cpl_phys == Equation_ustruct) && !cpl_flwP) { + throw std::runtime_error( + std::string("[read_bc] Follower pressure load must be used for 0D coupling with struct/ustruct on face '") + + face_name + "'."); + } + + // Get cap face VTP file name if defined + std::string zd_cap; + bool use_cap = false; + if (bc_params->coupling_interface.chamber_cap_surface.defined()) { + zd_cap = bc_params->coupling_interface.chamber_cap_surface.value(); + use_cap = true; + } + + if (use_cap) { + lBc.coupled_bc = CoupledBoundaryCondition(coupled_bc_type, com_mod.msh[lBc.iM].fa[lBc.iFa], + com_mod.msh[lBc.iM].fa[lBc.iFa].name, zd_block, zd_cap, + lEq.phys, cpl_flwP); + } else { + lBc.coupled_bc = CoupledBoundaryCondition(coupled_bc_type, com_mod.msh[lBc.iM].fa[lBc.iFa], + com_mod.msh[lBc.iM].fa[lBc.iFa].name, zd_block, lEq.phys, + cpl_flwP); + } } else { - lBc.coupled_bc = CoupledBoundaryCondition(coupled_bc_type, com_mod.msh[lBc.iM].fa[lBc.iFa], - com_mod.msh[lBc.iM].fa[lBc.iFa].name, zd_block, lEq.phys, - cpl_flwP); + throw std::runtime_error( + std::string("[read_bc] bType_Coupled is set on face '") + face_name + + "' but neither svZeroDSolver_interface nor svOneDSolver_interface is defined."); } } diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index a0006f8d6..c15542c3d 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -105,10 +105,16 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& } } - // Compute flowrates at 3D Neumann0D boundaries at timesteps n and n+1 for Coupled BCs + // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs if (utils::btest(bc.bType, iBC_Coupled)) { - bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); - #ifdef debug_calc_der_cpl_bc + if (cplBC.useSv1D && + bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { + // For svOneD DIR coupling the 1D solver needs the 3D face pressure. + bc.coupled_bc.compute_pressures(com_mod, cm_mod, solutions); + } else { + bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + } + #ifdef debug_calc_der_cpl_bc dmsg << "iBC_Coupled "; dmsg << "coupled_bc.Qo: " << bc.coupled_bc.get_Qo(); dmsg << "coupled_bc.Qn: " << bc.coupled_bc.get_Qn(); @@ -275,7 +281,11 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& // Perturb flowrate and compute new pressure bc.coupled_bc.perturb_flowrate(diff); - svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + if (cplBC.useSv1D) { + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + } else { + svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + } // Finite difference: dP/dQ bc.r = (bc.coupled_bc.get_pressure() - orig_state.pressure) / diff; @@ -772,9 +782,15 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) } - // Compute flowrates at 3D Neumann0D boundaries at timesteps n and n+1 for Coupled BCs + // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs if (utils::btest(bc.bType, iBC_Coupled)) { - bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + if (cplBC.useSv1D && + bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { + // For svOneD DIR coupling the 1D solver needs the 3D face pressure. + bc.coupled_bc.compute_pressures(com_mod, cm_mod, solutions); + } else { + bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + } } if (ptr != -1) { diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 3d9b8f85e..c0dea0572 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -43,7 +43,8 @@ // concurrently. // Phase 2 – batch result exchange: // - After every rank has finished solving, results are shared via -// MPI_Bcast so that ALL ranks know ALL cplBC.fa[i].y values. +// MPI_Bcast so that ALL ranks know the result, and each BC's +// coupled_bc.set_pressure() is updated accordingly. // // params array passed to run_1d_simulation_step_1d_: // params[0] = 2.0 (number of time points) @@ -72,9 +73,8 @@ namespace svOneD { // --------------------------------------------------------------------------- // Per-model state. Each entry corresponds to one svOneD-coupled face (one 1D -// model). Indexed by the sequential order in which faces were added to -// cplBC.fa (i.e., entry k corresponds to the k-th svOneD face, which has -// cplBCptr == face_ptr_list[k]). +// model). Indexed by the sequential order in which coupled faces were found +// in eq[0].bc[]. // --------------------------------------------------------------------------- struct OneDModelState { @@ -99,8 +99,8 @@ struct OneDModelState { // Owning MPI rank for this model. int owner_rank = 0; - // Index into cplBC.fa[] that this model services. - int fa_ptr = -1; + // Index into eq[0].bc[] for the BC this model services. + int iBc = -1; }; // --------------------------------------------------------------------------- @@ -148,21 +148,19 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) svOneDTime = com_mod.time; // ----- Collect the list of svOneD-coupled faces ----- - // We iterate over eq[0]'s BCs and pick those with iBC_cpl AND a non-empty - // oned_input_file. Their cplBCptr values are the indices into cplBC.fa[]. + // Iterate over eq[0]'s BCs and pick those with iBC_Coupled and a non-empty + // oned_input_file (stored in coupled_bc). { const int iEq = 0; const auto& eq = com_mod.eq[iEq]; for (int iBc = 0; iBc < eq.nBc; iBc++) { const auto& bc = eq.bc[iBc]; - int ptr = bc.cplBCptr; - if (ptr == -1) continue; - if (!utils::btest(bc.bType, iBC_cpl)) continue; - if (cplBC.fa[ptr].oned_input_file.empty()) continue; + if (!utils::btest(bc.bType, iBC_Coupled)) continue; + if (bc.coupled_bc.get_oned_input_file().empty()) continue; OneDModelState st; - st.fa_ptr = ptr; - st.coupling_type = (cplBC.fa[ptr].bGrp == CplBCType::cplBC_Neu) ? "NEU" : "DIR"; + st.iBc = iBc; + st.coupling_type = (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) ? "NEU" : "DIR"; oned_models.push_back(std::move(st)); } } @@ -195,6 +193,8 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) // reading and initializing only the model(s) it owns. Rank k owns model k // (assigned via k % nProcs), so for N models and N ranks every rank handles // exactly one model with no inter-rank synchronization. + const int iEq = 0; + auto& eq = com_mod.eq[iEq]; for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; st.owner_rank = k % nProcs; @@ -202,7 +202,7 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) if (myRank != st.owner_rank) continue; // This rank owns model k: read the input file and initialize. - const std::string& input_file = cplBC.fa[st.fa_ptr].oned_input_file; + const std::string& input_file = eq.bc[st.iBc].coupled_bc.get_oned_input_file(); int problem_id = 0; int system_size = 0; @@ -219,8 +219,8 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) st.solution.resize(system_size, 0.0); shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); - // Initial cplBC.fa y = 0; first calc_svOneD call sets the real value. - cplBC.fa[st.fa_ptr].y = 0.0; + // Initial coupled value = 0; first calc_svOneD call sets the real value. + eq.bc[st.iBc].coupled_bc.set_pressure(0.0); } // ----- Broadcast metadata for all models (Phase 2: batch exchange) ----- @@ -246,7 +246,6 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) { using namespace consts; - auto& cplBC = com_mod.cplBC; auto& cm = com_mod.cm; const int myRank = cm.taskId; @@ -254,6 +253,9 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) const double t_new = svOneDTime + com_mod.dt; const int nTotalModels = static_cast(oned_models.size()); + const int iEq = 0; + auto& eq = com_mod.eq[iEq]; + // ----- Phase 1: each rank runs its own models without blocking ----- // All ranks proceed through this loop simultaneously, each executing only // the models it owns. No MPI call here, so model k on rank A and model k+1 @@ -261,8 +263,8 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) std::vector cpl_values(nTotalModels, 0.0); for (int k = 0; k < nTotalModels; k++) { - auto& st = oned_models[k]; - int fa_ptr = st.fa_ptr; + auto& st = oned_models[k]; + auto& bc = eq.bc[st.iBc]; if (myRank != st.owner_rank) continue; @@ -272,12 +274,12 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) params[1] = t_old; params[2] = t_new; - if (cplBC.fa[fa_ptr].bGrp == CplBCType::cplBC_Neu) { - params[3] = cplBC.fa[fa_ptr].Qo; - params[4] = cplBC.fa[fa_ptr].Qn; + if (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { + params[3] = bc.coupled_bc.get_Qo(); + params[4] = bc.coupled_bc.get_Qn(); } else { - params[3] = cplBC.fa[fa_ptr].Po; - params[4] = cplBC.fa[fa_ptr].Pn; + params[3] = bc.coupled_bc.get_Po(); + params[4] = bc.coupled_bc.get_Pn(); } // Working copy of solution so that 'D' steps don't corrupt the @@ -295,7 +297,7 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (error_code != 0) { throw std::runtime_error( "[svOneD::calc_svOneD] 1D solver step for face '" + - cplBC.fa[fa_ptr].name + "' failed with error code " + + bc.coupled_bc.get_oned_input_file() + "' failed with error code " + std::to_string(error_code)); } @@ -305,14 +307,14 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) } } - // ----- Phase 2: broadcast all results and update cplBC.fa[].y ----- + // ----- Phase 2: broadcast all results and update coupled BCs ----- // After every rank has finished solving its own models, gather the results. // Each MPI_Bcast here is a cheap scalar transfer; the expensive 1D solver // work has already been done concurrently in Phase 1. for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; MPI_Bcast(&cpl_values[k], 1, MPI_DOUBLE, st.owner_rank, cm.com()); - cplBC.fa[st.fa_ptr].y = cpl_values[k]; + eq.bc[st.iBc].coupled_bc.set_pressure(cpl_values[k]); } // Advance the simulation clock after the final iteration. From f3d398fdfa81acd4efec5f9852f3a0d0389a87a3 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:35 -0700 Subject: [PATCH 13/51] Fix missing Qo_ in copy/move constructors and assignment operators --- Code/Source/solver/CoupledBoundaryCondition.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 7c4f035b8..3eff0a60f 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -55,6 +55,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound block_name_ = other.block_name_; face_name_ = other.face_name_; oned_input_file_ = other.oned_input_file_; + Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; Pn_ = other.Pn_; @@ -85,6 +86,7 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , block_name_(std::move(other.block_name_)) , face_name_(std::move(other.face_name_)) , oned_input_file_(std::move(other.oned_input_file_)) + , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) , Pn_(other.Pn_) @@ -131,6 +133,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon block_name_ = std::move(other.block_name_); face_name_ = std::move(other.face_name_); oned_input_file_ = std::move(other.oned_input_file_); + Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; Pn_ = other.Pn_; From 73f5d911e43369ee7075f4b31298245007a44619 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:36 -0700 Subject: [PATCH 14/51] Fix library name typo in svOneD_subroutines.cpp comment --- Code/Source/solver/svOneD_subroutines.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index c0dea0572..91758a87b 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -5,7 +5,7 @@ // // These routines interface the 3D finite-element solver (svMultiPhysics) with // the 1D blood-flow solver (svOneDSolver) via a dynamically loaded shared -// library (libsvoned_interface.so/.dylib). +// library (libsvOneDSolver_interface.so/.dylib). // // Coupling overview // ----------------- From 07883f646638796a81d56392b6274a58e73edbf1 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:36 -0700 Subject: [PATCH 15/51] fix DIR coupling bug: set_bc.cpp uses get_Qn() for DIR, svOneD calls set_flowrates() for DIR --- Code/Source/solver/set_bc.cpp | 11 +++++++++-- Code/Source/solver/svOneD_subroutines.cpp | 11 ++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index c15542c3d..d606536eb 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -852,9 +852,16 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) auto& bc = eq.bc[iBc]; int iFa = bc.iFa; - // For Coupled BC, get pressure from CoupledBoundaryCondition (set by svZeroD interface) + // For Coupled BC, get the coupling value from CoupledBoundaryCondition: + // NEU: 0D/1D solver returns pressure P → apply as Neumann traction + // DIR: 0D/1D solver returns flow rate Q → apply as Dirichlet velocity (bc.g = Q, then + // set_bc_dir_l multiplies by gx(a)*nV to produce the nodal velocity profile) if (utils::btest(bc.bType, iBC_Coupled)) { - bc.g = bc.coupled_bc.get_pressure(); + if (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Dir) { + bc.g = bc.coupled_bc.get_Qn(); + } else { + bc.g = bc.coupled_bc.get_pressure(); + } } // For other coupled BCs (Dir, Neu), get from cplBC.fa else { diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 91758a87b..ddafaa9e2 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -314,7 +314,16 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) for (int k = 0; k < nTotalModels; k++) { auto& st = oned_models[k]; MPI_Bcast(&cpl_values[k], 1, MPI_DOUBLE, st.owner_rank, cm.com()); - eq.bc[st.iBc].coupled_bc.set_pressure(cpl_values[k]); + auto& cpl_bc = eq.bc[st.iBc].coupled_bc; + if (cpl_bc.get_bc_type() == BoundaryConditionType::bType_Dir) { + // 1D solver returns flow Q for DIR coupling; store it as flowrate so that + // set_bc can read get_Qn() and build the nodal velocity profile. + double Qo_prev = cpl_bc.get_Qn(); + cpl_bc.set_flowrates(Qo_prev, cpl_values[k]); + } else { + // 1D solver returns pressure P for NEU coupling. + cpl_bc.set_pressure(cpl_values[k]); + } } // Advance the simulation clock after the final iteration. From e7b5135ca05d2264f288dbb5cc920ac3f664ed9c Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:37 -0700 Subject: [PATCH 16/51] fix DIR Coupled BCs: route to set_bc_dir_l (velocity), exclude from set_bc_neu_l (pressure) --- Code/Source/solver/set_bc.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index d606536eb..64e7ba2da 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -938,7 +938,12 @@ void set_bc_dir(ComMod& com_mod, SolutionStates& solutions) } } // END bType_CMM - if (!utils::btest(bc.bType, iBC_Dir)) { + // Allow Coupled BCs whose internal coupling type is DIR (svZeroD/svOneD DIR + // coupling): iBC_Dir is cleared for these in read_files but the velocity + // profile must still be applied via set_bc_dir_l. + bool isCoupledDir = utils::btest(bc.bType, iBC_Coupled) && + (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Dir); + if (!utils::btest(bc.bType, iBC_Dir) && !isCoupledDir) { continue; } @@ -1447,7 +1452,11 @@ void set_bc_neu(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& solu if (utils::btest(bc.bType, iBC_Ris0D)) {continue;} - if (utils::btest(bc.bType, iBC_Neu) || utils::btest(bc.bType, iBC_Coupled)) { + // Coupled BCs with DIR type must be handled by set_bc_dir (velocity profile), + // not here. Only NEU Coupled BCs get a Neumann pressure traction. + bool isCoupledDir = utils::btest(bc.bType, iBC_Coupled) && + (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Dir); + if ((utils::btest(bc.bType, iBC_Neu) || utils::btest(bc.bType, iBC_Coupled)) && !isCoupledDir) { #ifdef debug_set_bc_neu dmsg << "iM: " << iM+1; dmsg << "iFa: " << iFa+1; From 852b821de569f3fa759eb36291177037fef889fa Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:38 -0700 Subject: [PATCH 17/51] fix Bug 5: compute_pressures() for any DIR Coupled BC (not just svOneD), fixes svZeroD DIR --- Code/Source/solver/set_bc.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 64e7ba2da..29fad6f43 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -104,12 +104,13 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& RCRflag = true; } } - - // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs + + // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs. + // DIR coupling (both svZeroD and svOneD): the downstream solver is driven + // by the 3D face average pressure, so compute_pressures() is required. + // NEU coupling: the downstream solver is driven by the 3D outflow Q. if (utils::btest(bc.bType, iBC_Coupled)) { - if (cplBC.useSv1D && - bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { - // For svOneD DIR coupling the 1D solver needs the 3D face pressure. + if (bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { bc.coupled_bc.compute_pressures(com_mod, cm_mod, solutions); } else { bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); @@ -781,12 +782,12 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) } } - - // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs + // Compute flowrates/pressures at 3D coupled boundaries for Coupled BCs. + // DIR coupling (both svZeroD and svOneD): the downstream solver is driven + // by the 3D face average pressure, so compute_pressures() is required. + // NEU coupling: the downstream solver is driven by the 3D outflow Q. if (utils::btest(bc.bType, iBC_Coupled)) { - if (cplBC.useSv1D && - bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { - // For svOneD DIR coupling the 1D solver needs the 3D face pressure. + if (bc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { bc.coupled_bc.compute_pressures(com_mod, cm_mod, solutions); } else { bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); From 5119968f76435b9628a401b78b37276ece02616a Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:39 -0700 Subject: [PATCH 18/51] fix Bug 6: auto-enforce bType_flx for DIR Coupled BCs so Q/area gives m/s velocity --- Code/Source/solver/read_files.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 8e30ef82d..0811b623d 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -538,6 +538,17 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_flx)); } + // For DIR Coupled BCs (svZeroD or svOneD), the downstream solver always returns a + // volumetric flow rate Q [m³/s], not a velocity [m/s]. Without bType_flx, bc_ini + // sets gx(a) = 1, and set_bc_dir_l applies velocity = Q * nV which has the wrong + // dimensions. With bType_flx, bc_ini normalises gx(a) = 1/area, so the applied + // velocity = (Q/area) * nV [m/s] is physically correct. Enforce this automatically + // regardless of the user's setting. + if (utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled)) && + coupled_bc_type == BoundaryConditionType::bType_Dir) { + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_flx)); + } + // To zero-out perimeter or not. Default is .true. for Dir/CMM // ltmp = false; From 2f497426c653c0b67fc23e598feb27e31e4bf310 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:39 -0700 Subject: [PATCH 19/51] feat: enable svZeroD (0D) + RCR coexistence, mirroring svOneD behavior --- Code/Source/solver/set_bc.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 29fad6f43..03f364831 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -181,6 +181,10 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svZeroD faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else if (cplBC.useSv1D) { svOneD::calc_svOneD(com_mod, cm_mod, 'D'); // Also integrate any RCR faces that coexist with svOneD faces. @@ -235,6 +239,10 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svZeroD faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else if (cplBC.useSv1D) { svOneD::calc_svOneD(com_mod, cm_mod, 'D'); // Also integrate any RCR faces that coexist with svOneD faces. @@ -838,6 +846,10 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); } else if (cplBC.useSvZeroD){ svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + // Also integrate any RCR faces that coexist with svZeroD faces. + if (RCRflag) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else if (cplBC.useSv1D) { svOneD::calc_svOneD(com_mod, cm_mod, 'D'); // Also integrate any RCR faces that coexist with svOneD faces. From 0c012dc01070f703179f183a5966a48f75156cb8 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:40 -0700 Subject: [PATCH 20/51] fix: allow RCR BC to coexist with svZeroD coupling, fix scheme overwrite --- Code/Source/solver/read_files.cpp | 10 +++++----- Code/Source/solver/txt.cpp | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 0811b623d..661b7b2a3 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -351,9 +351,9 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, lBc.RCR.Pd = bc_params->rcr.distal_pressure.value(); lBc.RCR.Xo = bc_params->rcr.initial_pressure.value(); - if ((com_mod.cplBC.schm != CplBCType::cplBC_NA && !com_mod.cplBC.useSv1D) || + if ((com_mod.cplBC.schm != CplBCType::cplBC_NA && !com_mod.cplBC.useSv1D && !com_mod.cplBC.useSvZeroD) || com_mod.cplBC.xo.size() != 0) { - throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC (except alongside svOneD)."); + throw std::runtime_error("[read_bc] RCR cannot be used in conjunction with cplBC (except alongside svOneD or svZeroD)."); } com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; lBc.cplBCptr = com_mod.cplBC.nFa - 1; @@ -1625,9 +1625,9 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) if (std::set{Equation_fluid,Equation_FSI,Equation_CMM}.count(lEq.phys) == 0) { throw std::runtime_error("RCR-type BC is allowed for fluid/CMM/FSI eq. only."); } - // Only set coupling scheme if not already configured by an external solver (e.g. svOneD). - // When svOneD and RCR coexist, svOneD owns the scheme; RCR uses the same scheme. - if (!cplBC.useSv1D) { + // Only set coupling scheme if not already configured by an external solver (e.g. svOneD, svZeroD). + // When svOneD/svZeroD and RCR coexist, the external solver owns the scheme; RCR uses the same scheme. + if (!cplBC.useSv1D && !cplBC.useSvZeroD) { cplBC.schm = CplBCType::cplBC_SI; if (lEq.useTLS) { cplBC.schm = CplBCType::cplBC_E; diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index 9d02be893..3c93d882d 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -177,6 +177,16 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so set_bc::genBC_Integ_X(com_mod, cm_mod, "L"); } else if (cplBC.useSvZeroD) { svZeroD::calc_svZeroD(com_mod, cm_mod, 'L'); + // Also integrate any RCR faces that coexist with svZeroD faces. + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_RCR)) { + ltmp = true; + break; + } + } + if (ltmp) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } else if (cplBC.useSv1D) { svOneD::calc_svOneD(com_mod, cm_mod, 'L'); From c74ceebccde33722a4bf12c9116fd1cc439ac8cf Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:41 -0700 Subject: [PATCH 21/51] fix: distribute cplBC.xo to slave processes when svZeroD+RCR coexist --- Code/Source/solver/distribute.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 234b27c74..920a0d574 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -557,11 +557,16 @@ void distribute(Simulation* simulation) cplBC.xo.resize(cplBC.nX); } - } else if (cplBC.useSvZeroD) { - if (cm.slv(cm_mod)) { - cplBC.nX = 0; + } else if (cplBC.useSvZeroD) { + // Broadcast nX and xo: when RCR faces coexist with svZeroD, nX > 0 and xo must + // be distributed to all slave processes so rcr_init can access cplBC.xo[ptr]. + cm.bcast(cm_mod, &cplBC.nX); + if (cplBC.xo.size() == 0) { cplBC.xo.resize(cplBC.nX); } + if (cplBC.nX != 0) { + cm.bcast(cm_mod, cplBC.xo); + } } else { // RCR (Windkessel): nX/xo sized in read_files from nFa; not genBC/svZeroD. From 24adc2e265bb99ad715d1653bff6c40d217a73d8 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:42 -0700 Subject: [PATCH 22/51] fix: broadcast useSv1D to slaves and add RCR integration in sv1D txt.cpp branch --- Code/Source/solver/distribute.cpp | 1 + Code/Source/solver/txt.cpp | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 920a0d574..5ed3a7bcb 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -550,6 +550,7 @@ void distribute(Simulation* simulation) cm.bcast_enum(cm_mod, &cplBC.schm); cm.bcast(cm_mod, &cplBC.useGenBC); cm.bcast(cm_mod, &cplBC.useSvZeroD); + cm.bcast(cm_mod, &cplBC.useSv1D); if (cplBC.useGenBC) { if (cm.slv(cm_mod)) { diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index 3c93d882d..9aba6afa1 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -190,7 +190,17 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so } else if (cplBC.useSv1D) { svOneD::calc_svOneD(com_mod, cm_mod, 'L'); - + // Also integrate any RCR faces that coexist with svOneD faces. + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_RCR)) { + ltmp = true; + break; + } + } + if (ltmp) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } + } else { for (auto& bc : com_mod.eq[0].bc) { if (utils::btest(bc.bType, iBC_RCR)) { From 7c4f484a4c75d6d716bd595ac716e23cc488c630 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:42 -0700 Subject: [PATCH 23/51] fix: broadcast sv1d_solver_interface data to all MPI ranks in distribute.cpp --- Code/Source/solver/distribute.cpp | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 5ed3a7bcb..4e06ecfc4 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -568,12 +568,30 @@ void distribute(Simulation* simulation) if (cplBC.nX != 0) { cm.bcast(cm_mod, cplBC.xo); } + + } else if (cplBC.useSv1D) { + // Broadcast the sv1D solver interface data so that ALL ranks can call + // init_svOneD / calc_svOneD (which use MPI_Bcast collectives that require + // every rank to participate). Without this, slave processes have + // has_data = false and throw immediately inside init_svOneD. + cm.bcast(cm_mod, &cplBC.sv1d_solver_interface.has_data); + cm.bcast(cm_mod, cplBC.sv1d_solver_interface.solver_library); + + // Broadcast nX and xo: when RCR faces coexist with svOneD, nX > 0 and xo + // must be distributed to all slave processes so rcr_init works correctly. + cm.bcast(cm_mod, &cplBC.nX); + if (cplBC.xo.size() == 0) { + cplBC.xo.resize(cplBC.nX); + } + if (cplBC.nX != 0) { + cm.bcast(cm_mod, cplBC.xo); + } } else { - // RCR (Windkessel): nX/xo sized in read_files from nFa; not genBC/svZeroD. + // RCR (Windkessel): nX/xo sized in read_files from nFa; not genBC/svZeroD/svOneD. cm.bcast(cm_mod, &cplBC.nX); if (cplBC.xo.size() == 0) { - cplBC.xo.resize(cplBC.nX); + cplBC.xo.resize(cplBC.nX); } if (cplBC.nX != 0) { cm.bcast(cm_mod, cplBC.xo); From 283f6686359cc0ecefdd2dc9e0c6d806958925ea Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:43 -0700 Subject: [PATCH 24/51] fix: update run_1d_simulation_step_1d to pass last_flag and save_incr --- .../svOneD_interface/OneDSolverInterface.cpp | 16 +++++++++++----- .../svOneD_interface/OneDSolverInterface.h | 12 ++++++++---- Code/Source/solver/svOneD_subroutines.cpp | 6 +++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp b/Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp index bae1c9ec1..da6e098c2 100644 --- a/Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp +++ b/Code/Source/solver/svOneD_interface/OneDSolverInterface.cpp @@ -84,17 +84,23 @@ void OneDSolverInterface::update_solution(int problem_id, double* solution, int } void OneDSolverInterface::run_step(int problem_id, double current_time, - int save_flag, + int save_incr, const std::string& coupling_type, double* params, double* solution, - double& cpl_value, int& error_code) + double& cpl_value, char last_flag, + int& error_code) { if (!run_1d_simulation_step_1d_) { throw std::runtime_error("[OneDSolverInterface] run_1d_simulation_step_1d not loaded"); } - run_1d_simulation_step_1d_(problem_id, current_time, save_flag, - coupling_type.c_str(), params, solution, - cpl_value, error_code); + // Copy coupling_type into a mutable buffer (shared-library uses char*). + std::vector ctype_buf(coupling_type.begin(), coupling_type.end()); + ctype_buf.push_back('\0'); + // Copy last_flag into a mutable single-character buffer. + char flag_buf[2] = { last_flag, '\0' }; + run_1d_simulation_step_1d_(problem_id, current_time, save_incr, + ctype_buf.data(), params, solution, + cpl_value, flag_buf, error_code); } void OneDSolverInterface::extract_coupled_dof(int problem_id, int& coupled_dof, diff --git a/Code/Source/solver/svOneD_interface/OneDSolverInterface.h b/Code/Source/solver/svOneD_interface/OneDSolverInterface.h index 974589d38..29fcb3dd3 100644 --- a/Code/Source/solver/svOneD_interface/OneDSolverInterface.h +++ b/Code/Source/solver/svOneD_interface/OneDSolverInterface.h @@ -50,16 +50,20 @@ class OneDSolverInterface { /// @brief Advance the 1D solver by one time step. /// @param problem_id Problem identifier. /// @param current_time Current simulation time (start of the step). - /// @param save_flag Non-zero to write VTK output for this step. + /// @param save_incr VTK output interval (Increment_in_saving_VTK_files from solver.xml); + /// the 1D library decides internally whether to write output. /// @param coupling_type "NEU" or "DIR". /// @param params Array [N, t1, t2, ..., val1, val2, ...] where N=2. /// @param solution In/out: solution vector updated after the step. /// @param cpl_value Output: the BC value returned by the 1D solver /// (pressure for NEU, flow for DIR). + /// @param last_flag 'L' for the final (committed) iteration, 'D' for + /// derivative / predictor steps. /// @param error_code Output: non-zero on failure. - void run_step(int problem_id, double current_time, int save_flag, + void run_step(int problem_id, double current_time, int save_incr, const std::string& coupling_type, double* params, - double* solution, double& cpl_value, int& error_code); + double* solution, double& cpl_value, char last_flag, + int& error_code); /// @brief Retrieve the index within the solution vector that corresponds to /// the coupled boundary DOF. @@ -79,7 +83,7 @@ class OneDSolverInterface { void (*return_1d_solution_)(int, double*, int) = nullptr; void (*update_1d_solution_)(int, double*, int) = nullptr; void (*run_1d_simulation_step_1d_)(int, double, int, const char*, double*, - double*, double&, int&) = nullptr; + double*, double&, char*, int&) = nullptr; void (*extract_coupled_dof_)(int, int&, char*) = nullptr; }; diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index ddafaa9e2..054a61f6b 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -287,12 +287,12 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) std::vector work_sol = st.solution; st.interface->update_solution(st.problem_id, work_sol.data(), st.system_size); - int save_flag = (BCFlag == 'L') ? 1 : 0; + int save_incr = com_mod.saveIncr; int error_code = 0; - st.interface->run_step(st.problem_id, t_old, save_flag, + st.interface->run_step(st.problem_id, t_old, save_incr, st.coupling_type, params, - work_sol.data(), cpl_values[k], error_code); + work_sol.data(), cpl_values[k], BCFlag, error_code); if (error_code != 0) { throw std::runtime_error( From 81c9a318cf0bd34be9e6015afa76252df6844d9d Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:44 -0700 Subject: [PATCH 25/51] fix: recompute NEU coupling Qn_ from converged Yn before 1D L-step commit --- Code/Source/solver/txt.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index 9aba6afa1..76f6b1308 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -189,6 +189,20 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so } } else if (cplBC.useSv1D) { + // Update NEU coupling flowrates from the final converged velocity + // field (Yn) before committing the 1D solution. During the Newton + // loop, compute_flowrates() is called at the *start* of each + // iteration using a partially-converged Yn. After the last picc + // correction Yn is fully converged, but Qn_ is never refreshed. + // Re-computing here ensures the 1D solver receives the same + // flowrate that write_boundary_integral_data() reports in + // B_NS_Velocity_flux.txt. + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_Coupled) && + bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { + bc.coupled_bc.compute_flowrates(com_mod, cm_mod); + } + } svOneD::calc_svOneD(com_mod, cm_mod, 'L'); // Also integrate any RCR faces that coexist with svOneD faces. for (auto& bc : com_mod.eq[0].bc) { From 543d0a4023616e4cfcf2317697ab77059cceeff0 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:45 -0700 Subject: [PATCH 26/51] fix: negate 1D solver flow for DIR coupling to match svZeroD sign convention --- Code/Source/solver/svOneD_subroutines.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 054a61f6b..0dbc7bd36 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -318,8 +318,13 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (cpl_bc.get_bc_type() == BoundaryConditionType::bType_Dir) { // 1D solver returns flow Q for DIR coupling; store it as flowrate so that // set_bc can read get_Qn() and build the nodal velocity profile. + // Negate the sign: the 1D solver returns Q > 0 for inflow, but the 3D + // code applies velocity as Q * gx * outward_normal, so Q must be negative + // for an inlet face (outward normal points away from the domain). + // This matches the svZeroD convention: in_out = -1 for DIR (outlet of 0D + // = inlet of 3D), giving QCoupled = -1 * lpn_state_y[flow_id]. double Qo_prev = cpl_bc.get_Qn(); - cpl_bc.set_flowrates(Qo_prev, cpl_values[k]); + cpl_bc.set_flowrates(Qo_prev, -cpl_values[k]); } else { // 1D solver returns pressure P for NEU coupling. cpl_bc.set_pressure(cpl_values[k]); From a0ed4cac6451c23b709ec41267081856cdfa1fc1 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:45 -0700 Subject: [PATCH 27/51] fix: set bType_zp for Coupled-DIR BCs to preserve flow rate --- Code/Source/solver/read_files.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 661b7b2a3..07a4c8d29 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -549,10 +549,20 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_flx)); } - // To zero-out perimeter or not. Default is .true. for Dir/CMM + // To zero-out perimeter or not. Default is .true. for Dir/CMM and Coupled-DIR. + // For Coupled-DIR BCs, bType_Dir is cleared, but we still need bType_zp so that + // bc_ini zeros shared perimeter nodes before normalising gx. Without bType_zp, + // gx = 1/full_area and the wall BC later zeros perimeter nodes, reducing the + // effective flux to Q*(interior_area/full_area) instead of Q. // ltmp = false; ltmp = utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Dir)); + // Also enable zero-perimeter for Coupled-DIR (svZeroD or svOneD DIR coupling). + if (!ltmp && + utils::btest(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled)) && + coupled_bc_type == BoundaryConditionType::bType_Dir) { + ltmp = true; + } if (bc_params->zero_out_perimeter.defined()) { ltmp = bc_params->zero_out_perimeter.value(); } From 8327f4c933ec806cccc34cb40f0023b823845bc7 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:47 -0700 Subject: [PATCH 28/51] fix: register Coupled-DIR face as BC_TYPE_Dir in fsi_ls_ini to exclude DOFs from linear solve For 1D Dirichlet coupling, `iBC_Dir` is cleared in read_files.cpp so that set_bc_cpl routes the BC correctly. However this caused fsi_ls_ini to skip registering the face with the FSILS linear solver as a Dirichlet (BC_TYPE_Dir) constraint. As a result, the preconditioner did not zero out those rows/columns, and the linear solver overwrote the velocity values applied by set_bc_dir with the NS solution, producing a lower-than-expected centerline velocity (~47 vs ~63.66). Fix: detect Coupled-DIR faces in fsi_ls_ini and register them as BC_TYPE_Dir so their DOFs are properly excluded from Ax=b. --- Code/Source/solver/baf_ini.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index 4f18f177f..3fd00789c 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -776,10 +776,21 @@ void fsi_ls_ini(ComMod& com_mod, const CmMod& cm_mod, bcType& lBc, const faceTyp } fsils_bc_create(com_mod.lhs, lsPtr, lFa.nNo, nsd, BcType::BC_TYPE_Dir, gNodes, sVl); } + + } else if (btest(lBc.bType, iBC_Neu) || btest(lBc.bType, iBC_Coupled)) { + // For Coupled-DIR BCs: iBC_Dir was cleared in read_files but the face DOFs must still + // be excluded from the linear solve (Ax=b). Register as BC_TYPE_Dir so the + // preconditioner zeros out those rows/columns, preventing the solver from + // overwriting the velocity values set by set_bc_dir. + if (btest(lBc.bType, iBC_Coupled) && + lBc.coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { + lsPtr = lsPtr + 1; + lBc.lsPtr = lsPtr; + sVl = 0.0; + fsils_bc_create(com_mod.lhs, lsPtr, lFa.nNo, nsd, BcType::BC_TYPE_Dir, gNodes, sVl); - } else if (btest(lBc.bType, iBC_Neu)) { // Compute integral of normal vector over the face (needed for resistance BC/0D-coupling) - if (btest(lBc.bType, iBC_res)) { + } else if (btest(lBc.bType, iBC_res)) { sV = 0.0; for (int e = 0; e < lFa.nEl; e++) { if (lFa.eType == ElementType::NRB) { From ac5eb225b1215e38ad85cc66496baa5bf4eb59cb Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:48 -0700 Subject: [PATCH 29/51] feat: add pressure ramp for 1D coupling initialization (Method 2) - CouplingInterfaceParameters: add Coupling_ramp_steps (int) and Coupling_ramp_ref_pressure (double) XML parameters - CoupledBoundaryCondition: add oned_ramp_steps_ / oned_ramp_ref_pressure_ private members with set_oned_ramp() / get_oned_ramp_steps() / get_oned_ramp_ref_pressure() accessors; propagate through all copy/move constructors and assignment operators - read_files.cpp: read ramp params from CouplingInterfaceParameters and store them in lBc.coupled_bc via set_oned_ramp() - svOneD_subroutines.cpp: OneDModelState gains ramp_steps, ramp_ref_pressure, step_count fields; init_svOneD reads them from coupled_bc; calc_svOneD applies linear pressure ramp for DIR coupling; step_count is incremented on every committed (BCFlag=='L') step --- .../solver/CoupledBoundaryCondition.cpp | 8 ++++++ Code/Source/solver/CoupledBoundaryCondition.h | 17 ++++++++++++ Code/Source/solver/Parameters.cpp | 2 ++ Code/Source/solver/Parameters.h | 7 +++++ Code/Source/solver/read_files.cpp | 10 +++++++ Code/Source/solver/svOneD_subroutines.cpp | 26 +++++++++++++++++-- 6 files changed, 68 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 3eff0a60f..132415a20 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -23,6 +23,8 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryConditio , block_name_(other.block_name_) , face_name_(other.face_name_) , oned_input_file_(other.oned_input_file_) + , oned_ramp_steps_(other.oned_ramp_steps_) + , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -55,6 +57,8 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound block_name_ = other.block_name_; face_name_ = other.face_name_; oned_input_file_ = other.oned_input_file_; + oned_ramp_steps_ = other.oned_ramp_steps_; + oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; @@ -86,6 +90,8 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , block_name_(std::move(other.block_name_)) , face_name_(std::move(other.face_name_)) , oned_input_file_(std::move(other.oned_input_file_)) + , oned_ramp_steps_(other.oned_ramp_steps_) + , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -133,6 +139,8 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon block_name_ = std::move(other.block_name_); face_name_ = std::move(other.face_name_); oned_input_file_ = std::move(other.oned_input_file_); + oned_ramp_steps_ = other.oned_ramp_steps_; + oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index 4e376a41a..b01d54324 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -193,6 +193,9 @@ class CoupledBoundaryCondition { std::string face_name_; ///< Face name from the mesh /// @brief svOneD coupling data std::string oned_input_file_; ///< Path to svOneDSolver input file (empty for svZeroD BCs) + /// @brief Pressure ramp parameters for 1D coupling initialization (DIR coupling only). + int oned_ramp_steps_ = 0; ///< Number of ramp steps (0 = disabled) + double oned_ramp_ref_pressure_ = 0.0; ///< Reference pressure at step 0 /// @brief Flowrate data double Qo_ = 0.0; ///< Flowrate at old timestep (t_n) @@ -297,6 +300,20 @@ class CoupledBoundaryCondition { /// @brief Set the svOneD input file path void set_oned_input_file(const std::string& path); + + /// @brief Get the pressure ramp step count (0 = disabled). + int get_oned_ramp_steps() const { return oned_ramp_steps_; } + + /// @brief Set the pressure ramp parameters for 1D coupling initialization. + /// @param steps Number of time steps over which to ramp (0 = disabled). + /// @param P_ref Reference pressure at step 0 (typically the 1D initial pressure). + void set_oned_ramp(int steps, double P_ref) { + oned_ramp_steps_ = steps; + oned_ramp_ref_pressure_ = P_ref; + } + + /// @brief Get the ramp reference pressure. + double get_oned_ramp_ref_pressure() const { return oned_ramp_ref_pressure_; } /// @brief Set the svZeroD solution IDs for flow and pressure /// @param flow_id Flow solution ID diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 1e8e6e317..a76d0c409 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -469,6 +469,8 @@ CouplingInterfaceParameters::CouplingInterfaceParameters() set_parameter("svZeroDSolver_block", "", !required, svzerod_solver_block); set_parameter("Chamber_cap_surface", "", !required, chamber_cap_surface); set_parameter("svOneDSolver_input_file","", !required, svoned_input_file); + set_parameter("Coupling_ramp_steps", 0, !required, coupling_ramp_steps); + set_parameter("Coupling_ramp_ref_pressure", 0.0, !required, coupling_ramp_ref_pressure); } void CouplingInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 0eb060477..285a611de 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -806,6 +806,13 @@ class CouplingInterfaceParameters : public ParameterLists // Path to the svOneDSolver .in input file for this face (1D coupling). Parameter svoned_input_file; + // Pressure ramp for 1D coupling initialization (DIR coupling only). + // Over the first Coupling_ramp_steps time steps the pressure passed to the + // 1D solver is linearly ramped from Coupling_ramp_ref_pressure to the + // actual 3D pressure. Set Coupling_ramp_steps = 0 (default) to disable. + Parameter coupling_ramp_steps; + Parameter coupling_ramp_ref_pressure; + bool value_set = false; }; diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 07a4c8d29..cf88e7abf 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -462,6 +462,16 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, /*block_name=*/"", lEq.phys, /*follower_pressure_load=*/false); lBc.coupled_bc.set_oned_input_file(lBc.oned_input_file); + // Read optional pressure ramp parameters for 1D coupling initialization. + if (bc_params->coupling_interface.coupling_ramp_steps.defined() && + bc_params->coupling_interface.coupling_ramp_steps.value() > 0) { + int ramp_steps = bc_params->coupling_interface.coupling_ramp_steps.value(); + double ramp_P_ref = bc_params->coupling_interface.coupling_ramp_ref_pressure.defined() + ? bc_params->coupling_interface.coupling_ramp_ref_pressure.value() + : 0.0; + lBc.coupled_bc.set_oned_ramp(ramp_steps, ramp_P_ref); + } + } else if (com_mod.cplBC.svzerod_solver_interface.has_data) { // ------------------------------------------------------------------ // svZeroD path (existing). diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 0dbc7bd36..5253474db 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -101,6 +101,14 @@ struct OneDModelState { // Index into eq[0].bc[] for the BC this model services. int iBc = -1; + + // Pressure ramp for 1D coupling initialization (DIR coupling only). + // Over the first ramp_steps committed time steps the pressure sent to the + // 1D solver is linearly interpolated from ramp_ref_pressure to the actual + // 3D pressure value. Zero means no ramping. + int ramp_steps = 0; + double ramp_ref_pressure = 0.0; + int step_count = 0; ///< Number of committed (BCFlag=='L') steps taken. }; // --------------------------------------------------------------------------- @@ -161,6 +169,8 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) OneDModelState st; st.iBc = iBc; st.coupling_type = (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) ? "NEU" : "DIR"; + st.ramp_steps = bc.coupled_bc.get_oned_ramp_steps(); + st.ramp_ref_pressure = bc.coupled_bc.get_oned_ramp_ref_pressure(); oned_models.push_back(std::move(st)); } } @@ -278,8 +288,17 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) params[3] = bc.coupled_bc.get_Qo(); params[4] = bc.coupled_bc.get_Qn(); } else { - params[3] = bc.coupled_bc.get_Po(); - params[4] = bc.coupled_bc.get_Pn(); + double raw_P_old = bc.coupled_bc.get_Po(); + double raw_P_new = bc.coupled_bc.get_Pn(); + if (st.ramp_steps > 0) { + double ramp_factor = std::min(1.0, static_cast(st.step_count) / st.ramp_steps); + double P_ref = st.ramp_ref_pressure; + params[3] = P_ref + ramp_factor * (raw_P_old - P_ref); + params[4] = P_ref + ramp_factor * (raw_P_new - P_ref); + } else { + params[3] = raw_P_old; + params[4] = raw_P_new; + } } // Working copy of solution so that 'D' steps don't corrupt the @@ -334,6 +353,9 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Advance the simulation clock after the final iteration. if (BCFlag == 'L') { svOneDTime += com_mod.dt; + for (auto& st : oned_models) { + st.step_count++; + } } } From 9ee81895ef64065a2c20fdf29be61ba63ed8132c Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:50 -0700 Subject: [PATCH 30/51] Add under-relaxation for DIR coupling pressure passed to 1D solver Implements exponential smoothing of the pressure boundary condition sent to the 1D solver (DIR coupling only): P_sent = omega * P_target + (1 - omega) * P_prev_sent where P_target has already been through the ramp filter. - Parameters.h/cpp: add Coupling_dir_relax_factor parameter (default 1.0) - CoupledBoundaryCondition.h/cpp: add oned_relax_factor_ field, getter, setter, copy/move and distribute() broadcast - read_files.cpp: parse and apply the new parameter - svOneD_subroutines.cpp: add relax_factor / P_prev_sent_old/new to OneDModelState; apply under-relaxation after ramp; update history only on BCFlag=='L' committed steps; NEU coupling untouched --- .../solver/CoupledBoundaryCondition.cpp | 11 ++++++- Code/Source/solver/CoupledBoundaryCondition.h | 12 +++++++ Code/Source/solver/Parameters.cpp | 1 + Code/Source/solver/Parameters.h | 5 +++ Code/Source/solver/read_files.cpp | 6 ++++ Code/Source/solver/svOneD_subroutines.cpp | 31 ++++++++++++++++--- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 132415a20..5e04a8533 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -25,6 +25,7 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryConditio , oned_input_file_(other.oned_input_file_) , oned_ramp_steps_(other.oned_ramp_steps_) , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) + , oned_relax_factor_(other.oned_relax_factor_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -59,6 +60,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound oned_input_file_ = other.oned_input_file_; oned_ramp_steps_ = other.oned_ramp_steps_; oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; + oned_relax_factor_ = other.oned_relax_factor_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; @@ -92,6 +94,7 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , oned_input_file_(std::move(other.oned_input_file_)) , oned_ramp_steps_(other.oned_ramp_steps_) , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) + , oned_relax_factor_(other.oned_relax_factor_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -141,6 +144,7 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon oned_input_file_ = std::move(other.oned_input_file_); oned_ramp_steps_ = other.oned_ramp_steps_; oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; + oned_relax_factor_ = other.oned_relax_factor_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; @@ -425,7 +429,12 @@ void CoupledBoundaryCondition::distribute(const ComMod& com_mod, const CmMod& cm // Distribute 1D input file path cm.bcast(cm_mod, oned_input_file_); - + + // Distribute 1D ramp and relaxation parameters + cm.bcast(cm_mod, &oned_ramp_steps_); + cm.bcast(cm_mod, &oned_ramp_ref_pressure_); + cm.bcast(cm_mod, &oned_relax_factor_); + // Distribute face name cm.bcast(cm_mod, face_name_); diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index b01d54324..a52d0375e 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -196,6 +196,11 @@ class CoupledBoundaryCondition { /// @brief Pressure ramp parameters for 1D coupling initialization (DIR coupling only). int oned_ramp_steps_ = 0; ///< Number of ramp steps (0 = disabled) double oned_ramp_ref_pressure_ = 0.0; ///< Reference pressure at step 0 + + /// @brief Under-relaxation factor for pressure passed to the 1D solver (DIR coupling only). + /// Applied as: P_sent = omega * P_new + (1 - omega) * P_prev_sent. + /// Range: (0, 1]. Default 1.0 = no relaxation. + double oned_relax_factor_ = 1.0; /// @brief Flowrate data double Qo_ = 0.0; ///< Flowrate at old timestep (t_n) @@ -314,6 +319,13 @@ class CoupledBoundaryCondition { /// @brief Get the ramp reference pressure. double get_oned_ramp_ref_pressure() const { return oned_ramp_ref_pressure_; } + + /// @brief Get the under-relaxation factor for DIR coupling pressure (1.0 = no relaxation). + double get_oned_relax_factor() const { return oned_relax_factor_; } + + /// @brief Set the under-relaxation factor for DIR coupling pressure. + /// @param omega Relaxation factor in (0, 1]. 1.0 disables relaxation. + void set_oned_relax_factor(double omega) { oned_relax_factor_ = omega; } /// @brief Set the svZeroD solution IDs for flow and pressure /// @param flow_id Flow solution ID diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index a76d0c409..e0b9d3dc7 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -471,6 +471,7 @@ CouplingInterfaceParameters::CouplingInterfaceParameters() set_parameter("svOneDSolver_input_file","", !required, svoned_input_file); set_parameter("Coupling_ramp_steps", 0, !required, coupling_ramp_steps); set_parameter("Coupling_ramp_ref_pressure", 0.0, !required, coupling_ramp_ref_pressure); + set_parameter("Coupling_dir_relax_factor", 1.0, !required, coupling_dir_relax_factor); } void CouplingInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 285a611de..5a8497d6e 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -813,6 +813,11 @@ class CouplingInterfaceParameters : public ParameterLists Parameter coupling_ramp_steps; Parameter coupling_ramp_ref_pressure; + // Under-relaxation factor for pressure passed to the 1D solver (DIR coupling only). + // Applied as: P_sent = omega * P_new + (1 - omega) * P_prev_sent. + // Range: (0, 1]. Default 1.0 = no relaxation. + Parameter coupling_dir_relax_factor; + bool value_set = false; }; diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index cf88e7abf..52321b3a8 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -472,6 +472,12 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, lBc.coupled_bc.set_oned_ramp(ramp_steps, ramp_P_ref); } + // Read optional under-relaxation factor for DIR coupling pressure. + if (bc_params->coupling_interface.coupling_dir_relax_factor.defined()) { + double omega = bc_params->coupling_interface.coupling_dir_relax_factor.value(); + lBc.coupled_bc.set_oned_relax_factor(omega); + } + } else if (com_mod.cplBC.svzerod_solver_interface.has_data) { // ------------------------------------------------------------------ // svZeroD path (existing). diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 5253474db..3cc561332 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -109,6 +109,13 @@ struct OneDModelState { int ramp_steps = 0; double ramp_ref_pressure = 0.0; int step_count = 0; ///< Number of committed (BCFlag=='L') steps taken. + + // Under-relaxation for DIR coupling pressure (omega in (0, 1]). + // Applied after ramping: P_sent = omega * P_target + (1-omega) * P_prev_sent. + // Default 1.0 = no relaxation. + double relax_factor = 1.0; + double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step. + double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step. }; // --------------------------------------------------------------------------- @@ -171,6 +178,7 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) st.coupling_type = (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) ? "NEU" : "DIR"; st.ramp_steps = bc.coupled_bc.get_oned_ramp_steps(); st.ramp_ref_pressure = bc.coupled_bc.get_oned_ramp_ref_pressure(); + st.relax_factor = bc.coupled_bc.get_oned_relax_factor(); oned_models.push_back(std::move(st)); } } @@ -290,15 +298,25 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) } else { double raw_P_old = bc.coupled_bc.get_Po(); double raw_P_new = bc.coupled_bc.get_Pn(); + + // Step 1: apply pressure ramp (scales amplitude from ramp_ref_pressure + // to actual 3D pressure over the first ramp_steps steps). + double P_target_old, P_target_new; if (st.ramp_steps > 0) { double ramp_factor = std::min(1.0, static_cast(st.step_count) / st.ramp_steps); double P_ref = st.ramp_ref_pressure; - params[3] = P_ref + ramp_factor * (raw_P_old - P_ref); - params[4] = P_ref + ramp_factor * (raw_P_new - P_ref); + P_target_old = P_ref + ramp_factor * (raw_P_old - P_ref); + P_target_new = P_ref + ramp_factor * (raw_P_new - P_ref); } else { - params[3] = raw_P_old; - params[4] = raw_P_new; + P_target_old = raw_P_old; + P_target_new = raw_P_new; } + + // Step 2: apply under-relaxation (damps timestep-to-timestep oscillations). + // P_sent = omega * P_target + (1 - omega) * P_prev_sent + const double omega = st.relax_factor; + params[3] = omega * P_target_old + (1.0 - omega) * st.P_prev_sent_old; + params[4] = omega * P_target_new + (1.0 - omega) * st.P_prev_sent_new; } // Working copy of solution so that 'D' steps don't corrupt the @@ -323,6 +341,11 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Commit the updated solution only on the final iteration. if (BCFlag == 'L') { st.solution = work_sol; + // Update the under-relaxation history with the values actually sent. + if (st.coupling_type != "NEU") { + st.P_prev_sent_old = params[3]; + st.P_prev_sent_new = params[4]; + } } } From d9657a40a6f49ee54caa8e66e973f4cfbae4e68c Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:51 -0700 Subject: [PATCH 31/51] Apply under-relaxation to Q output for 1D Dirichlet coupling --- Code/Source/solver/svOneD_subroutines.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 3cc561332..99fff3421 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -110,12 +110,14 @@ struct OneDModelState { double ramp_ref_pressure = 0.0; int step_count = 0; ///< Number of committed (BCFlag=='L') steps taken. - // Under-relaxation for DIR coupling pressure (omega in (0, 1]). - // Applied after ramping: P_sent = omega * P_target + (1-omega) * P_prev_sent. + // Under-relaxation for DIR coupling (omega in (0, 1]). + // Applied after ramping for pressure input: P_sent = omega * P_target + (1-omega) * P_prev_sent. + // Also applied to flow rate output: Q_relaxed = omega * Q_raw + (1-omega) * Q_prev_sent. // Default 1.0 = no relaxation. double relax_factor = 1.0; double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step. double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step. + double Q_prev_sent = 0.0; ///< Under-relaxed flow rate output on last 'L' step (DIR coupling only). }; // --------------------------------------------------------------------------- @@ -365,8 +367,15 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // for an inlet face (outward normal points away from the domain). // This matches the svZeroD convention: in_out = -1 for DIR (outlet of 0D // = inlet of 3D), giving QCoupled = -1 * lpn_state_y[flow_id]. + double Q_raw = -cpl_values[k]; + // Apply under-relaxation to the Q output to damp timestep-to-timestep oscillations. + const double omega = st.relax_factor; + double Qn_relaxed = omega * Q_raw + (1.0 - omega) * st.Q_prev_sent; double Qo_prev = cpl_bc.get_Qn(); - cpl_bc.set_flowrates(Qo_prev, -cpl_values[k]); + cpl_bc.set_flowrates(Qo_prev, Qn_relaxed); + if (BCFlag == 'L') { + st.Q_prev_sent = Qn_relaxed; + } } else { // 1D solver returns pressure P for NEU coupling. cpl_bc.set_pressure(cpl_values[k]); From 5566f850374abbc5c6d5f756f2931b12b714bb65 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:52 -0700 Subject: [PATCH 32/51] feat: add under-relaxation to NEU coupling pressure output --- Code/Source/solver/svOneD_subroutines.cpp | 27 ++++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 99fff3421..bffdf27bf 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -110,14 +110,19 @@ struct OneDModelState { double ramp_ref_pressure = 0.0; int step_count = 0; ///< Number of committed (BCFlag=='L') steps taken. - // Under-relaxation for DIR coupling (omega in (0, 1]). - // Applied after ramping for pressure input: P_sent = omega * P_target + (1-omega) * P_prev_sent. - // Also applied to flow rate output: Q_relaxed = omega * Q_raw + (1-omega) * Q_prev_sent. - // Default 1.0 = no relaxation. + // Under-relaxation (omega in (0, 1]). Default 1.0 = no relaxation. + // + // DIR coupling: + // Input (P sent to 1D): P_sent = omega * P_target + (1-omega) * P_prev_sent + // Output (Q from 1D) : Q_relax = omega * Q_raw + (1-omega) * Q_prev_sent + // + // NEU coupling: + // Output (P from 1D) : P_relax = omega * P_raw + (1-omega) * P_neu_prev double relax_factor = 1.0; - double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step. - double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step. - double Q_prev_sent = 0.0; ///< Under-relaxed flow rate output on last 'L' step (DIR coupling only). + double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step (DIR). + double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step (DIR). + double Q_prev_sent = 0.0; ///< Under-relaxed flow rate output on last 'L' step (DIR only). + double P_neu_prev = 0.0; ///< Under-relaxed pressure output on last 'L' step (NEU only). }; // --------------------------------------------------------------------------- @@ -378,7 +383,13 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) } } else { // 1D solver returns pressure P for NEU coupling. - cpl_bc.set_pressure(cpl_values[k]); + // Apply under-relaxation to damp timestep-to-timestep oscillations. + const double omega = st.relax_factor; + double P_relaxed = omega * cpl_values[k] + (1.0 - omega) * st.P_neu_prev; + cpl_bc.set_pressure(P_relaxed); + if (BCFlag == 'L') { + st.P_neu_prev = P_relaxed; + } } } From 097bbd36f6a6e64ba52399ef7e1edcf1d62f2789 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:53 -0700 Subject: [PATCH 33/51] remove -dir at relax_factor --- Code/Source/solver/Parameters.cpp | 2 +- Code/Source/solver/Parameters.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index e0b9d3dc7..6ae85489c 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -471,7 +471,7 @@ CouplingInterfaceParameters::CouplingInterfaceParameters() set_parameter("svOneDSolver_input_file","", !required, svoned_input_file); set_parameter("Coupling_ramp_steps", 0, !required, coupling_ramp_steps); set_parameter("Coupling_ramp_ref_pressure", 0.0, !required, coupling_ramp_ref_pressure); - set_parameter("Coupling_dir_relax_factor", 1.0, !required, coupling_dir_relax_factor); + set_parameter("Coupling_relax_factor", 1.0, !required, coupling_relax_factor); } void CouplingInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 5a8497d6e..2c042ed6d 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -816,7 +816,7 @@ class CouplingInterfaceParameters : public ParameterLists // Under-relaxation factor for pressure passed to the 1D solver (DIR coupling only). // Applied as: P_sent = omega * P_new + (1 - omega) * P_prev_sent. // Range: (0, 1]. Default 1.0 = no relaxation. - Parameter coupling_dir_relax_factor; + Parameter coupling_relax_factor; bool value_set = false; }; From c727c57c4b7522520e4110511835b43eafff52fd Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:54 -0700 Subject: [PATCH 34/51] error at the previous commit. this is correct version after remove -dir in relax_factor --- Code/Source/solver/read_files.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 52321b3a8..0fba1dfd9 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -473,8 +473,8 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } // Read optional under-relaxation factor for DIR coupling pressure. - if (bc_params->coupling_interface.coupling_dir_relax_factor.defined()) { - double omega = bc_params->coupling_interface.coupling_dir_relax_factor.value(); + if (bc_params->coupling_interface.coupling_relax_factor.defined()) { + double omega = bc_params->coupling_interface.coupling_relax_factor.value(); lBc.coupled_bc.set_oned_relax_factor(omega); } From 4f8601aec23fa5756fc83fc0df55dcdc054e183b Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 17 Apr 2026 15:16:55 -0700 Subject: [PATCH 35/51] =?UTF-8?q?Rebase=20onto=20collaborator=20cleanup=20?= =?UTF-8?q?(6a2a54b):=20svZeroD=5Fsubroutines=E2=86=92svZeroD=5Finterface,?= =?UTF-8?q?=20delete=20legacy=20sv0D=20code,=20keep=20all=20svOneD=203D-1D?= =?UTF-8?q?=20coupling=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solver/CoupledBoundaryCondition.cpp | 68 ++++++- Code/Source/solver/CoupledBoundaryCondition.h | 18 ++ Code/Source/solver/Parameters.cpp | 182 ++++++++++++++++++ Code/Source/solver/Parameters.h | 1 + Code/Source/solver/read_files.cpp | 3 + Code/Source/solver/set_bc.cpp | 3 +- 6 files changed, 267 insertions(+), 8 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 5e04a8533..d25249937 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -16,6 +16,8 @@ // CoupledBoundaryCondition // ========================================================================= +const CmMod CoupledBoundaryCondition::cm_mod_{}; + CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryCondition& other) : face_(other.face_) , cap_face_vtp_file_(other.cap_face_vtp_file_) @@ -909,8 +911,27 @@ CappingSurface::CappingSurface(const CappingSurface& other) , valM_(other.valM_) , normals_(other.normals_) { - if (other.face_) { - face_ = std::make_unique(*other.face_); + if (other.face_ != nullptr) { + try { + face_ = std::make_unique(*other.face_); + } catch (const std::exception& e) { + throw CappingSurfaceCopyException("[CappingSurface::copy constructor] Failed to copy face_: " + + std::string(e.what())); + } + if (face_ != nullptr) { + if (face_->nNo > 0 && face_->gN.size() != face_->nNo) { + throw CappingSurfaceCopyException("[CappingSurface::copy constructor] Invalid face_: gN.size()=" + + std::to_string(face_->gN.size()) + " != nNo=" + + std::to_string(face_->nNo)); + } + if (face_->nEl > 0 && face_->IEN.ncols() != face_->nEl) { + throw CappingSurfaceCopyException("[CappingSurface::copy constructor] Invalid face_: IEN.ncols()=" + + std::to_string(face_->IEN.ncols()) + " != nEl=" + + std::to_string(face_->nEl)); + } + } + } else { + face_.reset(); } } @@ -920,8 +941,13 @@ CappingSurface& CappingSurface::operator=(const CappingSurface& other) global_node_ids_ = other.global_node_ids_; valM_ = other.valM_; normals_ = other.normals_; - if (other.face_) { - face_ = std::make_unique(*other.face_); + if (other.face_ != nullptr) { + try { + face_ = std::make_unique(*other.face_); + } catch (const std::exception& e) { + throw CappingSurfaceCopyException("[CappingSurface::operator=] Failed to copy face_: " + + std::string(e.what())); + } } else { face_.reset(); } @@ -1020,7 +1046,7 @@ void CappingSurface::init_cap_face_quadrature(const ComMod& com_mod) try { if (nsd != cap_nsd_) { - throw CappingSurfaceBaseException("[CappingSurface::init_cap_face_quadrature] Cap surface requires nsd=3."); + throw CappingSurfaceGeometryException("[CappingSurface::init_cap_face_quadrature] Cap surface requires nsd=3."); } face_->nG = 1; @@ -1068,6 +1094,10 @@ Array CappingSurface::update_element_position_global(int e, consts::Mech { using namespace consts; + if (mesh_x.nrows() < cap_nsd_ || mesh_Do.nrows() < cap_nsd_ || mesh_Dn.nrows() < cap_nsd_) { + throw CappingSurfaceGeometryException( + "[CappingSurface::update_element_position_global] Mesh arrays must have at least 3 rows."); + } Array xl(cap_nsd_, face_->eNoN); for (int a = 0; a < face_->eNoN; a++) { @@ -1097,6 +1127,13 @@ Array CappingSurface::update_element_position_global(int e, consts::Mech /// @return The Jacobian and normal vector. std::pair> CappingSurface::compute_jacobian_and_normal(const Array& xl, int e, int g) const { + if (xl.nrows() != cap_nsd_ || xl.ncols() != face_->eNoN) { + throw CappingSurfaceGeometryException("[CappingSurface::compute_jacobian_and_normal] xl has wrong dimensions: " + + std::to_string(xl.nrows()) + "x" + std::to_string(xl.ncols()) + + " (expected " + std::to_string(cap_nsd_) + "x" + + std::to_string(face_->eNoN) + ")."); + } + // Get the shape function derivatives for the Gauss point. Array Nx_g = face_->Nx.rslice(g); Array xXi(cap_nsd_, cap_insd_); @@ -1118,7 +1155,7 @@ std::pair> CappingSurface::compute_jacobian_and_normal(co Jac = sqrt(utils::norm(n)); if (utils::is_zero(Jac)) { - throw CappingSurfaceBaseException("[CappingSurface::compute_jacobian_and_normal] Zero Jacobian at Gauss point " + + throw CappingSurfaceGeometryException("[CappingSurface::compute_jacobian_and_normal] Zero Jacobian at Gauss point " + std::to_string(g)); } @@ -1126,6 +1163,11 @@ std::pair> CappingSurface::compute_jacobian_and_normal(co // Check if the initial normals are provided and if they are valid. if (normals_.ncols() > 0 && normals_.nrows() == cap_nsd_) { + if (e < 0 || e >= normals_.ncols()) { + throw CappingSurfaceGeometryException("[CappingSurface::compute_jacobian_and_normal] Element index e=" + + std::to_string(e) + " is out of bounds for normals_ (ncols=" + + std::to_string(normals_.ncols()) + ")."); + } Vector n0(cap_nsd_); for (int i = 0; i < cap_nsd_; i++) { @@ -1209,7 +1251,19 @@ void CappingSurface::compute_valM(consts::MechanicalConfigurationType cfg, const auto [Jac, n] = compute_jacobian_and_normal(xl, e, g); for (int a = 0; a < face_->eNoN; a++) { int gnNo_idx = face_->IEN(a, e); - int cap_a = gnNo_to_cap_local.at(gnNo_idx); + auto it = gnNo_to_cap_local.find(gnNo_idx); + if (it == gnNo_to_cap_local.end()) { + throw CappingSurfaceAssemblyException("[CappingSurface::compute_valM] IEN entry (element " + + std::to_string(e) + ", node " + std::to_string(a) + + ") contains invalid gnNo index " + std::to_string(gnNo_idx) + + " not found in cap face nodes."); + } + int cap_a = it->second; + if (cap_a < 0 || cap_a >= cap_nNo) { + throw CappingSurfaceAssemblyException("[CappingSurface::compute_valM] Invalid cap face-local index cap_a=" + + std::to_string(cap_a) + " (cap_nNo=" + std::to_string(cap_nNo) + + ")"); + } for (int i = 0; i < cap_nsd_; i++) { valM_(i, cap_a) += face_->N(a, g) * face_->w(g) * Jac * n(i); } diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index a52d0375e..2ba180da6 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -98,6 +98,12 @@ class CappingSurfaceInvalidElementNodesException : public CappingSurfaceBaseExce std::to_string(eNoN) + " (expected " + std::to_string(expected) + ").") {} }; +/// @brief Geometry or Jacobian error during cap surface integration. +class CappingSurfaceGeometryException : public CappingSurfaceBaseException { +public: + explicit CappingSurfaceGeometryException(const std::string& detail) : CappingSurfaceBaseException(detail) {} +}; + /// @brief Cap face quadrature (shape functions on TRI3) setup failed. class CappingSurfaceQuadratureException : public CappingSurfaceBaseException { public: @@ -106,6 +112,18 @@ class CappingSurfaceQuadratureException : public CappingSurfaceBaseException { "functions: " + nested) {} }; +/// @brief Inconsistent cap connectivity or assembly indexing. +class CappingSurfaceAssemblyException : public CappingSurfaceBaseException { +public: + explicit CappingSurfaceAssemblyException(const std::string& detail) : CappingSurfaceBaseException(detail) {} +}; + +/// @brief Failure copying a \ref CappingSurface or its internal face. +class CappingSurfaceCopyException : public CappingSurfaceBaseException { +public: + explicit CappingSurfaceCopyException(std::string msg) : CappingSurfaceBaseException(std::move(msg)) {} +}; + /// @brief Capping surface geometry and integration for a coupled boundary. class CappingSurface { public: diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 6ae85489c..2434d97c3 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -1858,6 +1858,7 @@ void DomainParameters::print_parameters() fluid_viscosity.print_parameters(); solid_viscosity.print_parameters(); + } //------------ @@ -1975,6 +1976,187 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e */ } +////////////////////////////////////////////////////////// +// TTPInitialConditionsParameters // +////////////////////////////////////////////////////////// + +const std::string TTPInitialConditionsParameters::xml_element_name_ = "TTP_initial_conditions"; + +TTPInitialConditionsParameters::TTPInitialConditionsParameters() +{ +} + +void TTPInitialConditionsParameters::print_parameters() +{ + if (value_set) { + std::cout << std::endl; + std::cout << "TTP Initial Conditions Parameters" << std::endl; + std::cout << "---------------------------------" << std::endl; + initial_states.print_parameters(); + gating_variables.print_parameters(); + } +} + +void TTPInitialConditionsParameters::set_values(tinyxml2::XMLElement* xml_elem) +{ + using namespace tinyxml2; + std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; + + auto item = xml_elem->FirstChildElement(); + + while (item != nullptr) { + auto name = std::string(item->Value()); + + if (name == TTPInitialStatesParameters::xml_element_name_) { + initial_states.set_values(item); + value_set = true; + + } else if (name == TTPGatingVariablesParameters::xml_element_name_) { + gating_variables.set_values(item); + value_set = true; + + } else { + throw std::runtime_error(error_msg + name + "'."); + } + + item = item->NextSiblingElement(); + } + + if (!initial_states.defined()) { + throw std::runtime_error(xml_element_name_ + " requires an '" + + TTPInitialStatesParameters::xml_element_name_ + "' XML section."); + } + + if (!gating_variables.defined()) { + throw std::runtime_error(xml_element_name_ + " requires a '" + + TTPGatingVariablesParameters::xml_element_name_ + "' XML section."); + } +} + +////////////////////////////////////////////////////////// +// TTPInitialStatesParameters // +////////////////////////////////////////////////////////// + +const std::string TTPInitialStatesParameters::xml_element_name_ = "Initial_states"; + +TTPInitialStatesParameters::TTPInitialStatesParameters() +{ + bool required = true; + + set_parameter("V", -85.23, required, V); + set_parameter("K_i", 136.89, required, K_i); + set_parameter("Na_i", 8.6040, required, Na_i); + set_parameter("Ca_i", 1.26E-4, required, Ca_i); + set_parameter("Ca_ss", 3.6E-4, required, Ca_ss); + set_parameter("Ca_sr", 3.64, required, Ca_sr); + set_parameter("R_bar", 0.9073, required, R_bar); +} + +void TTPInitialStatesParameters::print_parameters() +{ + if (value_set) { + std::cout << " Initial States:" << std::endl; + auto params_name_value = get_parameter_list(); + for (auto& [key, value] : params_name_value) { + std::cout << " " << key << ": " << value << std::endl; + } + } +} + +void TTPInitialStatesParameters::set_values(tinyxml2::XMLElement* xml_elem) +{ + using namespace tinyxml2; + std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; + + auto item = xml_elem->FirstChildElement(); + + while (item != nullptr) { + auto name = std::string(item->Value()); + + if (item->GetText() != nullptr) { + auto value = item->GetText(); + try { + set_parameter_value(name, value); + value_set = true; + } catch (const std::bad_function_call& exception) { + throw std::runtime_error(error_msg + name + "'."); + } + } else { + throw std::runtime_error(error_msg + name + "'."); + } + + item = item->NextSiblingElement(); + } + + check_required(); +} + +////////////////////////////////////////////////////////// +// TTPGatingVariablesParameters // +////////////////////////////////////////////////////////// + +const std::string TTPGatingVariablesParameters::xml_element_name_ = "Gating_variables"; + +TTPGatingVariablesParameters::TTPGatingVariablesParameters() +{ + bool required = true; + + set_parameter("x_r1_rectifier", 6.21E-3, required, x_r1_rectifier); + set_parameter("x_r2_rectifier", 0.4712, required, x_r2_rectifier); + set_parameter("x_s_rectifier", 9.5E-3, required, x_s_rectifier); + + set_parameter("m_fast_Na", 1.72E-3, required, m_fast_Na); + set_parameter("h_fast_Na", 0.7444, required, h_fast_Na); + set_parameter("j_fast_Na", 0.7045, required, j_fast_Na); + + set_parameter("d_slow_in", 3.373E-5, required, d_slow_in); + set_parameter("f_slow_in", 0.7888, required, f_slow_in); + set_parameter("f2_slow_in", 0.9755, required, f2_slow_in); + set_parameter("fcass_slow_in", 0.9953, required, fcass_slow_in); + + set_parameter("s_out", 0.999998, required, s_out); + set_parameter("r_out", 2.42E-8, required, r_out); +} + +void TTPGatingVariablesParameters::print_parameters() +{ + if (value_set) { + std::cout << " Gating Variables:" << std::endl; + auto params_name_value = get_parameter_list(); + for (auto& [key, value] : params_name_value) { + std::cout << " " << key << ": " << value << std::endl; + } + } +} + +void TTPGatingVariablesParameters::set_values(tinyxml2::XMLElement* xml_elem) +{ + using namespace tinyxml2; + std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; + + auto item = xml_elem->FirstChildElement(); + + while (item != nullptr) { + auto name = std::string(item->Value()); + + if (item->GetText() != nullptr) { + auto value = item->GetText(); + try { + set_parameter_value(name, value); + value_set = true; + } catch (const std::bad_function_call& exception) { + throw std::runtime_error(error_msg + name + "'."); + } + } else { + throw std::runtime_error(error_msg + name + "'."); + } + + item = item->NextSiblingElement(); + } + + check_required(); +} + ////////////////////////////////////////////////////////// // DirectionalDistributionParameters // ////////////////////////////////////////////////////////// diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 2c042ed6d..3f2e45b1a 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -1402,6 +1402,7 @@ class DomainParameters : public ParameterLists StimulusParameters stimulus; FluidViscosityParameters fluid_viscosity; SolidViscosityParameters solid_viscosity; + TTPInitialConditionsParameters ttp_initial_conditions; // Ionic model parameters. std::map> ionic_models; diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 0fba1dfd9..0bb120e97 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -302,6 +302,8 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, " is defined on the equation."); } + + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; lBc.cplBCptr = com_mod.cplBC.nFa - 1; @@ -539,6 +541,7 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, com_mod.msh[lBc.iM].fa[lBc.iFa].name, zd_block, lEq.phys, cpl_flwP); } + } else { throw std::runtime_error( std::string("[read_bc] bType_Coupled is set on face '") + face_name + diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 03f364831..363fa803d 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -216,7 +216,7 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& diff = diff*relTol; } - // Store the original pressures and flowrates + // Store the original pressures and flowrates for cplBC std::vector orgY(cplBC.fa.size()); std::vector orgQ(cplBC.fa.size()); @@ -313,6 +313,7 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& } /// @brief RCR (Windkessel) integration for the non-genBC / non-svZeroD branch. +/// The legacy external Fortran Couple_to_cplBC file coupling has been removed. void cplBC_Integ_X(ComMod& com_mod, const CmMod& cm_mod, const bool RCRflag) { using namespace consts; From c1861a8b152dcb260970edf57884340028ce199f Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:38:11 -0700 Subject: [PATCH 36/51] Fix const-correctness in CappingSurface: face() const, mutable valM_, compute_valM/compute_jacobian_and_normal const --- Code/Source/solver/CoupledBoundaryCondition.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index 2ba180da6..de1b4f424 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -151,12 +151,11 @@ class CappingSurface { /// Surface velocity flux through the cap using \a st columns indexed by cap IEN / GlobalNodeID (master / serial). double integrate_velocity_flux(const CapGlobalMeshState& st, bool use_Yn_velocity, consts::MechanicalConfigurationType cfg); - - /// @brief Compute the cap contribution to the linear solver face (fills \ref valM_; safe under \c const *this). + + /// @brief Compute the cap contribution to the linear solver face (fills \ref valM_; safe under \c const *this). void compute_valM(consts::MechanicalConfigurationType cfg, const CapGlobalMeshState& st) const; /// @brief Get the cap face. - faceType* face() { return face_.get(); } const faceType* face() const { return face_.get(); } /// @brief Get the cap contribution. From cd0d072e5a45a0c3904e870965c443cfdae7d1eb Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:38:14 -0700 Subject: [PATCH 37/51] fix: broadcast DIR flowrate Qo/Qn to all MPI ranks in svZeroD coupling --- .../solver/CoupledBoundaryCondition.cpp | 20 +++++++++++++++++++ Code/Source/solver/CoupledBoundaryCondition.h | 3 +++ Code/Source/solver/svZeroD_interface.cpp | 3 +++ 3 files changed, 26 insertions(+) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index d25249937..63c46cffa 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -1399,3 +1399,23 @@ void CoupledBoundaryCondition::bcast_coupled_neumann_pressure(const CmMod& cm_mo cm.bcast(cm_mod, &pr); set_pressure(pr); } + +void CoupledBoundaryCondition::bcast_coupled_dir_flowrate(const CmMod& cm_mod, cmType& cm) +{ + if (cm.seq()) { + return; + } + using namespace consts; + if (get_bc_type() != BoundaryConditionType::bType_Dir) { + return; + } + double Qo = 0.0; + double Qn = 0.0; + if (cm.mas(cm_mod)) { + Qo = get_Qo(); + Qn = get_Qn(); + } + cm.bcast(cm_mod, &Qo); + cm.bcast(cm_mod, &Qn); + set_flowrates(Qo, Qn); +} diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index de1b4f424..c32cf220c 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -469,6 +469,9 @@ class CoupledBoundaryCondition { /// @brief Master reads Neumann pressure, one scalar \c MPI_Bcast, all ranks set pressure (svZeroD sync). void bcast_coupled_neumann_pressure(const CmMod& cm_mod, cmType& cm); + /// @brief Master reads DIR flowrates (Qo, Qn), two scalar \c MPI_Bcast, all ranks set flowrates (svZeroD sync). + void bcast_coupled_dir_flowrate(const CmMod& cm_mod, cmType& cm); + }; #endif // COUPLED_BOUNDARY_CONDITION_H diff --git a/Code/Source/solver/svZeroD_interface.cpp b/Code/Source/solver/svZeroD_interface.cpp index 470f5fff8..1d6293304 100644 --- a/Code/Source/solver/svZeroD_interface.cpp +++ b/Code/Source/solver/svZeroD_interface.cpp @@ -486,6 +486,9 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (utils::btest(bc.bType, iBC_Coupled) && bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { bc.coupled_bc.bcast_coupled_neumann_pressure(cm_mod, cm); + } else if (utils::btest(bc.bType, iBC_Coupled) && + bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Dir) { + bc.coupled_bc.bcast_coupled_dir_flowrate(cm_mod, cm); } } } From a6e0c22e8b75d491cf4e51ffb06dd2bf4534c693 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:38:15 -0700 Subject: [PATCH 38/51] feat: add ramp/relax support to 3D-0D coupling (svZeroD) --- .../solver/CoupledBoundaryCondition.cpp | 20 ++++++ Code/Source/solver/CoupledBoundaryCondition.h | 46 +++++++++++++- Code/Source/solver/read_files.cpp | 16 +++++ Code/Source/solver/svZeroD_interface.cpp | 61 +++++++++++++++++-- 4 files changed, 135 insertions(+), 8 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 63c46cffa..00c6e7150 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -28,6 +28,11 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryConditio , oned_ramp_steps_(other.oned_ramp_steps_) , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) , oned_relax_factor_(other.oned_relax_factor_) + , ramp_step_count_(other.ramp_step_count_) + , P_prev_sent_old_(other.P_prev_sent_old_) + , P_prev_sent_new_(other.P_prev_sent_new_) + , Q_prev_sent_(other.Q_prev_sent_) + , P_neu_prev_(other.P_neu_prev_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -63,6 +68,11 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound oned_ramp_steps_ = other.oned_ramp_steps_; oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; oned_relax_factor_ = other.oned_relax_factor_; + ramp_step_count_ = other.ramp_step_count_; + P_prev_sent_old_ = other.P_prev_sent_old_; + P_prev_sent_new_ = other.P_prev_sent_new_; + Q_prev_sent_ = other.Q_prev_sent_; + P_neu_prev_ = other.P_neu_prev_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; @@ -97,6 +107,11 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , oned_ramp_steps_(other.oned_ramp_steps_) , oned_ramp_ref_pressure_(other.oned_ramp_ref_pressure_) , oned_relax_factor_(other.oned_relax_factor_) + , ramp_step_count_(other.ramp_step_count_) + , P_prev_sent_old_(other.P_prev_sent_old_) + , P_prev_sent_new_(other.P_prev_sent_new_) + , Q_prev_sent_(other.Q_prev_sent_) + , P_neu_prev_(other.P_neu_prev_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -147,6 +162,11 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon oned_ramp_steps_ = other.oned_ramp_steps_; oned_ramp_ref_pressure_ = other.oned_ramp_ref_pressure_; oned_relax_factor_ = other.oned_relax_factor_; + ramp_step_count_ = other.ramp_step_count_; + P_prev_sent_old_ = other.P_prev_sent_old_; + P_prev_sent_new_ = other.P_prev_sent_new_; + Q_prev_sent_ = other.Q_prev_sent_; + P_neu_prev_ = other.P_neu_prev_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index c32cf220c..9fcad5179 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -218,7 +218,15 @@ class CoupledBoundaryCondition { /// Applied as: P_sent = omega * P_new + (1 - omega) * P_prev_sent. /// Range: (0, 1]. Default 1.0 = no relaxation. double oned_relax_factor_ = 1.0; - + + /// @brief Ramp/relax runtime state (shared by both 0D and 1D coupling). + /// Updated only on committed ('L') time steps. + int ramp_step_count_ = 0; ///< Number of committed steps taken (used for ramp fraction). + double P_prev_sent_old_ = 0.0; ///< Under-relaxed pressure sent at t_old on last 'L' step (DIR input). + double P_prev_sent_new_ = 0.0; ///< Under-relaxed pressure sent at t_new on last 'L' step (DIR input). + double Q_prev_sent_ = 0.0; ///< Under-relaxed flow output on last 'L' step (DIR output). + double P_neu_prev_ = 0.0; ///< Under-relaxed pressure output on last 'L' step (NEU output). + /// @brief Flowrate data double Qo_ = 0.0; ///< Flowrate at old timestep (t_n) double Qn_ = 0.0; ///< Flowrate at new timestep (t_{n+1}) @@ -343,7 +351,41 @@ class CoupledBoundaryCondition { /// @brief Set the under-relaxation factor for DIR coupling pressure. /// @param omega Relaxation factor in (0, 1]. 1.0 disables relaxation. void set_oned_relax_factor(double omega) { oned_relax_factor_ = omega; } - + + // ========================================================================= + // Ramp/relax runtime state accessors (shared by 0D and 1D coupling) + // ========================================================================= + + /// @brief Get the number of committed time steps (used for ramp fraction). + int get_ramp_step_count() const { return ramp_step_count_; } + + /// @brief Increment the committed step counter (call once per 'L' step). + void increment_ramp_step_count() { ramp_step_count_++; } + + /// @brief Get the under-relaxed pressure sent at t_old on the last 'L' step (DIR input). + double get_P_prev_sent_old() const { return P_prev_sent_old_; } + + /// @brief Get the under-relaxed pressure sent at t_new on the last 'L' step (DIR input). + double get_P_prev_sent_new() const { return P_prev_sent_new_; } + + /// @brief Set the DIR input pressure history (call on 'L' steps). + void set_P_prev_sent(double old_val, double new_val) { + P_prev_sent_old_ = old_val; + P_prev_sent_new_ = new_val; + } + + /// @brief Get the under-relaxed flow output on the last 'L' step (DIR output). + double get_Q_prev_sent() const { return Q_prev_sent_; } + + /// @brief Set the DIR output flow history (call on 'L' steps). + void set_Q_prev_sent(double Q) { Q_prev_sent_ = Q; } + + /// @brief Get the under-relaxed pressure output on the last 'L' step (NEU output). + double get_P_neu_prev() const { return P_neu_prev_; } + + /// @brief Set the NEU output pressure history (call on 'L' steps). + void set_P_neu_prev(double P) { P_neu_prev_ = P; } + /// @brief Set the svZeroD solution IDs for flow and pressure /// @param flow_id Flow solution ID /// @param pressure_id Pressure solution ID diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 0bb120e97..610652800 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -542,6 +542,22 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, cpl_flwP); } + // Read optional pressure ramp parameters for 0D coupling initialization. + if (bc_params->coupling_interface.coupling_ramp_steps.defined() && + bc_params->coupling_interface.coupling_ramp_steps.value() > 0) { + int ramp_steps = bc_params->coupling_interface.coupling_ramp_steps.value(); + double ramp_P_ref = bc_params->coupling_interface.coupling_ramp_ref_pressure.defined() + ? bc_params->coupling_interface.coupling_ramp_ref_pressure.value() + : 0.0; + lBc.coupled_bc.set_oned_ramp(ramp_steps, ramp_P_ref); + } + + // Read optional under-relaxation factor for 0D coupling. + if (bc_params->coupling_interface.coupling_relax_factor.defined()) { + double omega = bc_params->coupling_interface.coupling_relax_factor.value(); + lBc.coupled_bc.set_oned_relax_factor(omega); + } + } else { throw std::runtime_error( std::string("[read_bc] bType_Coupled is set on face '") + face_name + diff --git a/Code/Source/solver/svZeroD_interface.cpp b/Code/Source/solver/svZeroD_interface.cpp index 1d6293304..20e07741e 100644 --- a/Code/Source/solver/svZeroD_interface.cpp +++ b/Code/Source/solver/svZeroD_interface.cpp @@ -386,6 +386,12 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) double params[2]; double times[2]; int error_code; + + // Temporary arrays to record relaxed values for history update on 'L' steps. + std::vector P_sent_old_arr(numCoupledSrfs, 0.0); + std::vector P_sent_new_arr(numCoupledSrfs, 0.0); + std::vector Q_relaxed_arr(numCoupledSrfs, 0.0); + std::vector P_relaxed_arr(numCoupledSrfs, 0.0); get_coupled_QP(com_mod, QCoupled, QnCoupled, PCoupled, PnCoupled); @@ -416,8 +422,30 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) double sign = bc->coupled_bc.get_in_out_sign(); if (is_dirichlet) { - params[0] = PCoupled[i]; - params[1] = PnCoupled[i]; + double raw_P_old = PCoupled[i]; + double raw_P_new = PnCoupled[i]; + + // Step 1: apply pressure ramp (scales amplitude from ramp_ref_pressure + // to actual 3D pressure over the first ramp_steps steps). + int ramp_steps = bc->coupled_bc.get_oned_ramp_steps(); + double P_target_old, P_target_new; + if (ramp_steps > 0) { + double ramp_factor = std::min(1.0, static_cast(bc->coupled_bc.get_ramp_step_count()) / ramp_steps); + double P_ref = bc->coupled_bc.get_oned_ramp_ref_pressure(); + P_target_old = P_ref + ramp_factor * (raw_P_old - P_ref); + P_target_new = P_ref + ramp_factor * (raw_P_new - P_ref); + } else { + P_target_old = raw_P_old; + P_target_new = raw_P_new; + } + + // Step 2: apply under-relaxation (damps timestep-to-timestep oscillations). + // P_sent = omega * P_target + (1 - omega) * P_prev_sent + const double omega = bc->coupled_bc.get_oned_relax_factor(); + params[0] = omega * P_target_old + (1.0 - omega) * bc->coupled_bc.get_P_prev_sent_old(); + params[1] = omega * P_target_new + (1.0 - omega) * bc->coupled_bc.get_P_prev_sent_new(); + P_sent_old_arr[i] = params[0]; + P_sent_new_arr[i] = params[1]; } else { params[0] = sign * QCoupled[i]; params[1] = sign * QnCoupled[i]; @@ -447,12 +475,20 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) } if (bc->coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Neu) { - PCoupled[i] = lpn_state_y[pressure_id]; - bc->coupled_bc.set_pressure(PCoupled[i]); + double P_raw = lpn_state_y[pressure_id]; + // Apply under-relaxation to the pressure output. + const double omega = bc->coupled_bc.get_oned_relax_factor(); + double P_relaxed = omega * P_raw + (1.0 - omega) * bc->coupled_bc.get_P_neu_prev(); + bc->coupled_bc.set_pressure(P_relaxed); + P_relaxed_arr[i] = P_relaxed; } else if (bc->coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { - QCoupled[i] = in_out * lpn_state_y[flow_id]; + double Q_raw = in_out * lpn_state_y[flow_id]; + // Apply under-relaxation to the flow output to damp timestep-to-timestep oscillations. + const double omega = bc->coupled_bc.get_oned_relax_factor(); + double Q_relaxed = omega * Q_raw + (1.0 - omega) * bc->coupled_bc.get_Q_prev_sent(); double Qo_prev = bc->coupled_bc.get_Qn(); - bc->coupled_bc.set_flowrates(Qo_prev, QCoupled[i]); + bc->coupled_bc.set_flowrates(Qo_prev, Q_relaxed); + Q_relaxed_arr[i] = Q_relaxed; } else { throw std::runtime_error("ERROR: [calc_svZeroD] Invalid Coupled BC type."); } @@ -463,6 +499,19 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) interface->return_ydot(last_state_ydot); std::copy(lpn_state_y.begin(), lpn_state_y.end(), last_state_y.begin()); + // Update ramp/relax history for all coupled BCs. + for (int i = 0; i < numCoupledSrfs; ++i) { + bcType* bc_hist = nullptr; + if (!nth_coupled_bc(com_mod, i, &bc_hist)) continue; + if (bc_hist->coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Dir) { + bc_hist->coupled_bc.set_P_prev_sent(P_sent_old_arr[i], P_sent_new_arr[i]); + bc_hist->coupled_bc.set_Q_prev_sent(Q_relaxed_arr[i]); + } else { + bc_hist->coupled_bc.set_P_neu_prev(P_relaxed_arr[i]); + } + bc_hist->coupled_bc.increment_ramp_step_count(); + } + if (writeSvZeroD == 1) { // Write the state vector to a file int arg = 1; From df85a87ed82cf6e94961efc1381c888f0812ad3f Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:38:16 -0700 Subject: [PATCH 39/51] feat: add ramp/relax to NEU input (Q sent to 0D solver) Co-authored-by: taeoukkim <74677557+taeoukkim@users.noreply.github.com> --- .../solver/CoupledBoundaryCondition.cpp | 8 ++++++ Code/Source/solver/CoupledBoundaryCondition.h | 14 ++++++++++ Code/Source/solver/svZeroD_interface.cpp | 27 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index 00c6e7150..dabd324ca 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -33,6 +33,8 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryConditio , P_prev_sent_new_(other.P_prev_sent_new_) , Q_prev_sent_(other.Q_prev_sent_) , P_neu_prev_(other.P_neu_prev_) + , Q_input_prev_old_(other.Q_input_prev_old_) + , Q_input_prev_new_(other.Q_input_prev_new_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -73,6 +75,8 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(const CoupledBound P_prev_sent_new_ = other.P_prev_sent_new_; Q_prev_sent_ = other.Q_prev_sent_; P_neu_prev_ = other.P_neu_prev_; + Q_input_prev_old_ = other.Q_input_prev_old_; + Q_input_prev_new_ = other.Q_input_prev_new_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; @@ -112,6 +116,8 @@ CoupledBoundaryCondition::CoupledBoundaryCondition(CoupledBoundaryCondition&& ot , P_prev_sent_new_(other.P_prev_sent_new_) , Q_prev_sent_(other.Q_prev_sent_) , P_neu_prev_(other.P_neu_prev_) + , Q_input_prev_old_(other.Q_input_prev_old_) + , Q_input_prev_new_(other.Q_input_prev_new_) , Qo_(other.Qo_) , Qn_(other.Qn_) , Po_(other.Po_) @@ -167,6 +173,8 @@ CoupledBoundaryCondition& CoupledBoundaryCondition::operator=(CoupledBoundaryCon P_prev_sent_new_ = other.P_prev_sent_new_; Q_prev_sent_ = other.Q_prev_sent_; P_neu_prev_ = other.P_neu_prev_; + Q_input_prev_old_ = other.Q_input_prev_old_; + Q_input_prev_new_ = other.Q_input_prev_new_; Qo_ = other.Qo_; Qn_ = other.Qn_; Po_ = other.Po_; diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index 9fcad5179..e3da28a71 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -226,6 +226,8 @@ class CoupledBoundaryCondition { double P_prev_sent_new_ = 0.0; ///< Under-relaxed pressure sent at t_new on last 'L' step (DIR input). double Q_prev_sent_ = 0.0; ///< Under-relaxed flow output on last 'L' step (DIR output). double P_neu_prev_ = 0.0; ///< Under-relaxed pressure output on last 'L' step (NEU output). + double Q_input_prev_old_ = 0.0; ///< Under-relaxed flow sent at t_old on last 'L' step (NEU input). + double Q_input_prev_new_ = 0.0; ///< Under-relaxed flow sent at t_new on last 'L' step (NEU input). /// @brief Flowrate data double Qo_ = 0.0; ///< Flowrate at old timestep (t_n) @@ -386,6 +388,18 @@ class CoupledBoundaryCondition { /// @brief Set the NEU output pressure history (call on 'L' steps). void set_P_neu_prev(double P) { P_neu_prev_ = P; } + /// @brief Get the under-relaxed flow sent at t_old on the last 'L' step (NEU input). + double get_Q_input_prev_old() const { return Q_input_prev_old_; } + + /// @brief Get the under-relaxed flow sent at t_new on the last 'L' step (NEU input). + double get_Q_input_prev_new() const { return Q_input_prev_new_; } + + /// @brief Set the NEU input flow history (call on 'L' steps). + void set_Q_input_prev(double old_val, double new_val) { + Q_input_prev_old_ = old_val; + Q_input_prev_new_ = new_val; + } + /// @brief Set the svZeroD solution IDs for flow and pressure /// @param flow_id Flow solution ID /// @param pressure_id Pressure solution ID diff --git a/Code/Source/solver/svZeroD_interface.cpp b/Code/Source/solver/svZeroD_interface.cpp index 20e07741e..643b09c26 100644 --- a/Code/Source/solver/svZeroD_interface.cpp +++ b/Code/Source/solver/svZeroD_interface.cpp @@ -390,6 +390,8 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Temporary arrays to record relaxed values for history update on 'L' steps. std::vector P_sent_old_arr(numCoupledSrfs, 0.0); std::vector P_sent_new_arr(numCoupledSrfs, 0.0); + std::vector Q_input_sent_old_arr(numCoupledSrfs, 0.0); + std::vector Q_input_sent_new_arr(numCoupledSrfs, 0.0); std::vector Q_relaxed_arr(numCoupledSrfs, 0.0); std::vector P_relaxed_arr(numCoupledSrfs, 0.0); @@ -447,8 +449,28 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) P_sent_old_arr[i] = params[0]; P_sent_new_arr[i] = params[1]; } else { - params[0] = sign * QCoupled[i]; - params[1] = sign * QnCoupled[i]; + double raw_Q_old = sign * QCoupled[i]; + double raw_Q_new = sign * QnCoupled[i]; + + // Step 1: apply flow ramp (scales Q from ramp_ref over the first ramp_steps steps). + int ramp_steps = bc->coupled_bc.get_oned_ramp_steps(); + double Q_target_old, Q_target_new; + if (ramp_steps > 0) { + double ramp_factor = std::min(1.0, static_cast(bc->coupled_bc.get_ramp_step_count()) / ramp_steps); + double Q_ref = bc->coupled_bc.get_oned_ramp_ref_pressure(); // ref value (0.0 by default) + Q_target_old = Q_ref + ramp_factor * (raw_Q_old - Q_ref); + Q_target_new = Q_ref + ramp_factor * (raw_Q_new - Q_ref); + } else { + Q_target_old = raw_Q_old; + Q_target_new = raw_Q_new; + } + + // Step 2: apply under-relaxation. + const double omega = bc->coupled_bc.get_oned_relax_factor(); + params[0] = omega * Q_target_old + (1.0 - omega) * bc->coupled_bc.get_Q_input_prev_old(); + params[1] = omega * Q_target_new + (1.0 - omega) * bc->coupled_bc.get_Q_input_prev_new(); + Q_input_sent_old_arr[i] = params[0]; + Q_input_sent_new_arr[i] = params[1]; } update_svZeroD_block_params(svzd_blk_names[i], times, params); } @@ -507,6 +529,7 @@ void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) bc_hist->coupled_bc.set_P_prev_sent(P_sent_old_arr[i], P_sent_new_arr[i]); bc_hist->coupled_bc.set_Q_prev_sent(Q_relaxed_arr[i]); } else { + bc_hist->coupled_bc.set_Q_input_prev(Q_input_sent_old_arr[i], Q_input_sent_new_arr[i]); bc_hist->coupled_bc.set_P_neu_prev(P_relaxed_arr[i]); } bc_hist->coupled_bc.increment_ramp_step_count(); From afbbd467357b2701ccffa17d5c0e6d150c033cba Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Mon, 20 Apr 2026 16:30:02 -0700 Subject: [PATCH 40/51] fix bug --- Code/Source/solver/txt.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index 76f6b1308..ea637487c 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -200,7 +200,7 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so for (auto& bc : com_mod.eq[0].bc) { if (utils::btest(bc.bType, iBC_Coupled) && bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { - bc.coupled_bc.compute_flowrates(com_mod, cm_mod); + bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); } } svOneD::calc_svOneD(com_mod, cm_mod, 'L'); From b74fc99b658e6c42a8a0f63d06bddb509de2b59c Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 30 Apr 2026 16:43:44 -0700 Subject: [PATCH 41/51] fix neumann coupling under relaxation and ramping --- Code/Source/solver/Parameters.h | 15 +++++---- Code/Source/solver/svOneD_subroutines.cpp | 39 +++++++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 3f2e45b1a..a90fb44a4 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -806,15 +806,18 @@ class CouplingInterfaceParameters : public ParameterLists // Path to the svOneDSolver .in input file for this face (1D coupling). Parameter svoned_input_file; - // Pressure ramp for 1D coupling initialization (DIR coupling only). - // Over the first Coupling_ramp_steps time steps the pressure passed to the - // 1D solver is linearly ramped from Coupling_ramp_ref_pressure to the - // actual 3D pressure. Set Coupling_ramp_steps = 0 (default) to disable. + // Ramp for 1D coupling initialization (both DIR and NEU coupling). + // Over the first Coupling_ramp_steps committed time steps the value passed + // to the 1D solver is linearly ramped: + // DIR: pressure P ramped from Coupling_ramp_ref_pressure to actual 3D P. + // NEU: flow rate Q ramped from 0 to actual 3D Q (ref pressure not used). + // Set Coupling_ramp_steps = 0 (default) to disable. Parameter coupling_ramp_steps; Parameter coupling_ramp_ref_pressure; - // Under-relaxation factor for pressure passed to the 1D solver (DIR coupling only). - // Applied as: P_sent = omega * P_new + (1 - omega) * P_prev_sent. + // Under-relaxation factor for the value passed to the 1D solver (both DIR and NEU coupling). + // DIR: P_sent = omega * P_target + (1 - omega) * P_prev_sent + // NEU: Q_sent = omega * Q_target + (1 - omega) * Q_prev_sent // Range: (0, 1]. Default 1.0 = no relaxation. Parameter coupling_relax_factor; diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index bffdf27bf..10a93ff5b 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -102,10 +102,13 @@ struct OneDModelState { // Index into eq[0].bc[] for the BC this model services. int iBc = -1; - // Pressure ramp for 1D coupling initialization (DIR coupling only). - // Over the first ramp_steps committed time steps the pressure sent to the - // 1D solver is linearly interpolated from ramp_ref_pressure to the actual - // 3D pressure value. Zero means no ramping. + // Pressure ramp for 1D coupling initialization. + // DIR: over the first ramp_steps committed time steps the pressure sent to + // the 1D solver is linearly interpolated from ramp_ref_pressure to the + // actual 3D pressure value. + // NEU: over the first ramp_steps committed time steps the flow rate sent to + // the 1D solver is linearly interpolated from 0 to the actual 3D Q. + // Zero means no ramping. int ramp_steps = 0; double ramp_ref_pressure = 0.0; int step_count = 0; ///< Number of committed (BCFlag=='L') steps taken. @@ -117,12 +120,15 @@ struct OneDModelState { // Output (Q from 1D) : Q_relax = omega * Q_raw + (1-omega) * Q_prev_sent // // NEU coupling: + // Input (Q sent to 1D): Q_sent = omega * Q_target + (1-omega) * Q_prev_sent // Output (P from 1D) : P_relax = omega * P_raw + (1-omega) * P_neu_prev double relax_factor = 1.0; double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step (DIR). double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step (DIR). double Q_prev_sent = 0.0; ///< Under-relaxed flow rate output on last 'L' step (DIR only). double P_neu_prev = 0.0; ///< Under-relaxed pressure output on last 'L' step (NEU only). + double Q_prev_sent_old = 0.0; ///< Under-relaxed flow rate sent at params[3] (t_old) on last 'L' step (NEU). + double Q_prev_sent_new = 0.0; ///< Under-relaxed flow rate sent at params[4] (t_new) on last 'L' step (NEU). }; // --------------------------------------------------------------------------- @@ -300,8 +306,24 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) params[2] = t_new; if (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { - params[3] = bc.coupled_bc.get_Qo(); - params[4] = bc.coupled_bc.get_Qn(); + double raw_Q_old = bc.coupled_bc.get_Qo(); + double raw_Q_new = bc.coupled_bc.get_Qn(); + // Step 1: apply flow rate ramp (scales amplitude from 0 to actual 3D + // flow rate over the first ramp_steps committed steps). + double Q_target_old, Q_target_new; + if (st.ramp_steps > 0) { + double ramp_factor = std::min(1.0, static_cast(st.step_count) / st.ramp_steps); + Q_target_old = ramp_factor * raw_Q_old; + Q_target_new = ramp_factor * raw_Q_new; + } else { + Q_target_old = raw_Q_old; + Q_target_new = raw_Q_new; + } + // Step 2: apply under-relaxation (damps timestep-to-timestep oscillations). + // Q_sent = omega * Q_target + (1 - omega) * Q_prev_sent + const double omega = st.relax_factor; + params[3] = omega * Q_target_old + (1.0 - omega) * st.Q_prev_sent_old; + params[4] = omega * Q_target_new + (1.0 - omega) * st.Q_prev_sent_new; } else { double raw_P_old = bc.coupled_bc.get_Po(); double raw_P_new = bc.coupled_bc.get_Pn(); @@ -349,7 +371,10 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (BCFlag == 'L') { st.solution = work_sol; // Update the under-relaxation history with the values actually sent. - if (st.coupling_type != "NEU") { + if (st.coupling_type == "NEU") { + st.Q_prev_sent_old = params[3]; + st.Q_prev_sent_new = params[4]; + } else { st.P_prev_sent_old = params[3]; st.P_prev_sent_new = params[4]; } From f01218a9f366bb8d10febe04f6694895c1b5bda8 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Wed, 6 May 2026 16:30:58 -0700 Subject: [PATCH 42/51] change Neu to Robin to avoid back flow --- Code/Source/solver/set_bc.cpp | 33 ++++++++++++- Code/Source/solver/svOneD_subroutines.cpp | 57 +++++++++++++---------- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 363fa803d..33a0f2f88 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -1539,7 +1539,38 @@ void set_bc_neu_l(ComMod& com_mod, const CmMod& cm_mod, const bcType& lBc, const } } - } else if (utils::btest(lBc.bType,iBC_res)) { + } else if (utils::btest(lBc.bType,iBC_Coupled)) { + // New-style Coupled NEU BC (svOneD/svZeroD): apply the actual 1D/0D + // pressure from the most recent 'D' solver call. bc.g is updated + // every Newton iteration by set_bc_cpl / calc_der_cpl_bc. + + //h(0) = lBc.g; + + double Q_3D = all_fun::integ(com_mod, cm_mod, lFa, Yn, eq.s, solutions, + eq.s+nsd-1, false, + consts::MechanicalConfigurationType::reference); + //h(0) = lBc.g - lBc.r * Q_3D; + + // Double-sided Robin: use |Q_3D| so that the correction always + // reduces h from P_1D, regardless of flow direction. + h(0) = lBc.g - lBc.r * std::abs(Q_3D); + // Backflow kinetic energy correction: when backflow is detected + // (Q < 0), subtract the face-averaged dynamic pressure to further + // reduce the applied traction and damp the incoming flow. + if (Q_3D < 0.0) { + int iM = lFa.iM; + int cDmn_local = all_fun::domain(com_mod, com_mod.msh[iM], cEq, lFa.gE(0)); + double rho = eq.dmn[cDmn_local].prop.at( + consts::PhysicalProperyType::fluid_density); + double beta = eq.dmn[cDmn_local].prop.at( + consts::PhysicalProperyType::backflow_stab); + double A = lFa.area; + if (A > 0.0) { + double u_n = Q_3D / A; // face-averaged normal velocity (< 0) + h(0) -= 0.5 * beta * rho * u_n * u_n; + } + } + } else if (utils::btest(lBc.bType,iBC_res)) { h(0) = lBc.r * all_fun::integ(com_mod, cm_mod, lFa, Yn, eq.s, solutions, eq.s+nsd-1, false, consts::MechanicalConfigurationType::reference); } else if (utils::btest(lBc.bType,iBC_std)) { diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 10a93ff5b..8789617a8 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -120,15 +120,15 @@ struct OneDModelState { // Output (Q from 1D) : Q_relax = omega * Q_raw + (1-omega) * Q_prev_sent // // NEU coupling: - // Input (Q sent to 1D): Q_sent = omega * Q_target + (1-omega) * Q_prev_sent - // Output (P from 1D) : P_relax = omega * P_raw + (1-omega) * P_neu_prev + // Output (P from 1D) : P_applied = omega * P_target + (1-omega) * P_neu_prev + // (P_target already includes the ramp from ramp_ref_pressure to P_raw) double relax_factor = 1.0; double P_prev_sent_old = 0.0; ///< Under-relaxed pressure sent at params[3] (t_old) on last 'L' step (DIR). double P_prev_sent_new = 0.0; ///< Under-relaxed pressure sent at params[4] (t_new) on last 'L' step (DIR). double Q_prev_sent = 0.0; ///< Under-relaxed flow rate output on last 'L' step (DIR only). - double P_neu_prev = 0.0; ///< Under-relaxed pressure output on last 'L' step (NEU only). - double Q_prev_sent_old = 0.0; ///< Under-relaxed flow rate sent at params[3] (t_old) on last 'L' step (NEU). - double Q_prev_sent_new = 0.0; ///< Under-relaxed flow rate sent at params[4] (t_new) on last 'L' step (NEU). + double P_neu_prev = 0.0; ///< Under-relaxed pressure applied to 3D on last 'L' step (NEU only). + double Q_prev_sent_old = 0.0; ///< Under-relaxed Q sent at params[3] (t_old) on last 'L' step (NEU). + double Q_prev_sent_new = 0.0; ///< Under-relaxed Q sent at params[4] (t_new) on last 'L' step (NEU). }; // --------------------------------------------------------------------------- @@ -306,24 +306,13 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) params[2] = t_new; if (bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { - double raw_Q_old = bc.coupled_bc.get_Qo(); - double raw_Q_new = bc.coupled_bc.get_Qn(); - // Step 1: apply flow rate ramp (scales amplitude from 0 to actual 3D - // flow rate over the first ramp_steps committed steps). - double Q_target_old, Q_target_new; - if (st.ramp_steps > 0) { - double ramp_factor = std::min(1.0, static_cast(st.step_count) / st.ramp_steps); - Q_target_old = ramp_factor * raw_Q_old; - Q_target_new = ramp_factor * raw_Q_new; - } else { - Q_target_old = raw_Q_old; - Q_target_new = raw_Q_new; - } - // Step 2: apply under-relaxation (damps timestep-to-timestep oscillations). - // Q_sent = omega * Q_target + (1 - omega) * Q_prev_sent + // NEU coupling: apply under-relaxation to the 3D flow rate Q sent to + // the 1D solver to damp timestep-to-timestep oscillations in the input. + // No ramping is applied here; ramping is applied to the *output* pressure + // P that the 1D solver returns (Phase 2 below). const double omega = st.relax_factor; - params[3] = omega * Q_target_old + (1.0 - omega) * st.Q_prev_sent_old; - params[4] = omega * Q_target_new + (1.0 - omega) * st.Q_prev_sent_new; + params[3] = omega * bc.coupled_bc.get_Qo() + (1.0 - omega) * st.Q_prev_sent_new; + params[4] = omega * bc.coupled_bc.get_Qn() + (1.0 - omega) * st.Q_prev_sent_new; } else { double raw_P_old = bc.coupled_bc.get_Po(); double raw_P_new = bc.coupled_bc.get_Pn(); @@ -344,7 +333,7 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) // Step 2: apply under-relaxation (damps timestep-to-timestep oscillations). // P_sent = omega * P_target + (1 - omega) * P_prev_sent const double omega = st.relax_factor; - params[3] = omega * P_target_old + (1.0 - omega) * st.P_prev_sent_old; + params[3] = omega * P_target_old + (1.0 - omega) * st.P_prev_sent_new; params[4] = omega * P_target_new + (1.0 - omega) * st.P_prev_sent_new; } @@ -371,6 +360,8 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) if (BCFlag == 'L') { st.solution = work_sol; // Update the under-relaxation history with the values actually sent. + // NEU: Q_prev_sent updated here; P history updated in Phase 2. + // DIR: P_prev_sent tracks the pressure value sent to the 1D solver. if (st.coupling_type == "NEU") { st.Q_prev_sent_old = params[3]; st.Q_prev_sent_new = params[4]; @@ -408,9 +399,25 @@ void calc_svOneD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag) } } else { // 1D solver returns pressure P for NEU coupling. - // Apply under-relaxation to damp timestep-to-timestep oscillations. + // + // Step 1: apply pressure ramp (scales output P from ramp_ref_pressure + // to the actual 1D pressure over the first ramp_steps committed + // steps). This prevents a large sudden pressure jump from being + // imposed on the 3D domain at startup, which is the primary cause + // of oscillations in Neumann coupling. + double P_raw = cpl_values[k]; + double P_target; + if (st.ramp_steps > 0) { + double ramp_factor = std::min(1.0, static_cast(st.step_count) / st.ramp_steps); + double P_ref = st.ramp_ref_pressure; + P_target = P_ref + ramp_factor * (P_raw - P_ref); + } else { + P_target = P_raw; + } + // Step 2: apply under-relaxation to damp timestep-to-timestep oscillations. + // P_applied = omega * P_target + (1 - omega) * P_prev_applied const double omega = st.relax_factor; - double P_relaxed = omega * cpl_values[k] + (1.0 - omega) * st.P_neu_prev; + double P_relaxed = omega * P_target + (1.0 - omega) * st.P_neu_prev; cpl_bc.set_pressure(P_relaxed); if (BCFlag == 'L') { st.P_neu_prev = P_relaxed; From ff76805b49f64fab22f6b6af98d646aea238cc1d Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Tue, 12 May 2026 15:49:29 -0700 Subject: [PATCH 43/51] fix: remove robin part, keep only stabilization term. enough to converge --- Code/Source/solver/set_bc.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index 33a0f2f88..e283762b0 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -1549,11 +1549,11 @@ void set_bc_neu_l(ComMod& com_mod, const CmMod& cm_mod, const bcType& lBc, const double Q_3D = all_fun::integ(com_mod, cm_mod, lFa, Yn, eq.s, solutions, eq.s+nsd-1, false, consts::MechanicalConfigurationType::reference); - //h(0) = lBc.g - lBc.r * Q_3D; - // Double-sided Robin: use |Q_3D| so that the correction always - // reduces h from P_1D, regardless of flow direction. - h(0) = lBc.g - lBc.r * std::abs(Q_3D); + + h(0) = lBc.g; + //h(0) = lBc.g - lBc.r * std::abs(Q_3D); + // Backflow kinetic energy correction: when backflow is detected // (Q < 0), subtract the face-averaged dynamic pressure to further // reduce the applied traction and damp the incoming flow. From 42e3039e486579c2501ee0addb49bed799e2e854 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 15 May 2026 14:53:42 -0700 Subject: [PATCH 44/51] enhancement: available to use both 3D-1D coupling and 3D-0D coupling at the same model --- Code/Source/solver/CoupledBoundaryCondition.h | 2 + Code/Source/solver/distribute.cpp | 30 +--- Code/Source/solver/read_files.cpp | 147 ++++++++++++------ Code/Source/solver/set_bc.cpp | 52 ++++--- Code/Source/solver/svZeroD_interface.cpp | 8 +- Code/Source/solver/txt.cpp | 78 +++++----- 6 files changed, 176 insertions(+), 141 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.h b/Code/Source/solver/CoupledBoundaryCondition.h index e3da28a71..764877096 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.h +++ b/Code/Source/solver/CoupledBoundaryCondition.h @@ -333,6 +333,8 @@ class CoupledBoundaryCondition { /// @brief Set the svOneD input file path void set_oned_input_file(const std::string& path); + bool is_sv1d_face() const { return !oned_input_file_.empty(); } + /// @brief Get the pressure ramp step count (0 = disabled). int get_oned_ramp_steps() const { return oned_ramp_steps_; } diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 4e06ecfc4..8adc11a35 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -557,38 +557,20 @@ void distribute(Simulation* simulation) cplBC.nX = 0; cplBC.xo.resize(cplBC.nX); } + } - } else if (cplBC.useSvZeroD) { - // Broadcast nX and xo: when RCR faces coexist with svZeroD, nX > 0 and xo must - // be distributed to all slave processes so rcr_init can access cplBC.xo[ptr]. - cm.bcast(cm_mod, &cplBC.nX); - if (cplBC.xo.size() == 0) { - cplBC.xo.resize(cplBC.nX); - } - if (cplBC.nX != 0) { - cm.bcast(cm_mod, cplBC.xo); - } - - } else if (cplBC.useSv1D) { + if (cplBC.useSv1D) { // Broadcast the sv1D solver interface data so that ALL ranks can call // init_svOneD / calc_svOneD (which use MPI_Bcast collectives that require // every rank to participate). Without this, slave processes have // has_data = false and throw immediately inside init_svOneD. cm.bcast(cm_mod, &cplBC.sv1d_solver_interface.has_data); cm.bcast(cm_mod, cplBC.sv1d_solver_interface.solver_library); + } - // Broadcast nX and xo: when RCR faces coexist with svOneD, nX > 0 and xo - // must be distributed to all slave processes so rcr_init works correctly. - cm.bcast(cm_mod, &cplBC.nX); - if (cplBC.xo.size() == 0) { - cplBC.xo.resize(cplBC.nX); - } - if (cplBC.nX != 0) { - cm.bcast(cm_mod, cplBC.xo); - } - - } else { - // RCR (Windkessel): nX/xo sized in read_files from nFa; not genBC/svZeroD/svOneD. + if (!cplBC.useGenBC) { + // Broadcast nX and xo: when RCR faces coexist with svZeroD/svOneD, nX > 0 + // and xo must be distributed to all slave processes so rcr_init works correctly. cm.bcast(cm_mod, &cplBC.nX); if (cplBC.xo.size() == 0) { cplBC.xo.resize(cplBC.nX); diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index 610652800..c092624d1 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -236,46 +236,53 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, read_fourier_coeff_values_file(file_name, lBc); } - // Coupling to a 0D model: + // Coupling to a 0D/1D model: // - GenBC / cplBC: Time_dependence Coupled without (cplBC.fa). // - svZeroDSolver: Time_dependence Coupled + (CoupledBoundaryCondition). // } else if (ctmp == "Coupled") { auto& face_name = com_mod.msh[lBc.iM].fa[lBc.iFa].name; const bool svzd_iface = com_mod.cplBC.svzerod_solver_interface.has_data; + const bool sv1d_iface = com_mod.cplBC.sv1d_solver_interface.has_data; const bool ci_set = bc_params->coupling_interface.value_set; - const bool ci_has_block = ci_set && bc_params->coupling_interface.svzerod_solver_block.defined(); - - if (svzd_iface) { - // Coupled BC to svZeroDSolver - lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled)); - lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_bfs)); + const bool ci_has_block = ci_set && bc_params->coupling_interface.svzerod_solver_block.defined(); + const bool ci_has_1d_file = ci_set && bc_params->coupling_interface.svoned_input_file.defined(); // Sanity check: must define - if (!ci_has_block) { - if (ci_set) { - throw std::runtime_error(std::string("[read_bc] on face '") + face_name + - "' must define ."); - } + if (svzd_iface || sv1d_iface) { + // svZeroD and/or svOneD path: route each face individually based on its + // content. + + if (ci_has_block && ci_has_1d_file) { throw std::runtime_error( - std::string("[read_bc] With , each svZeroD-coupled face needs " - " with (Time_dependence Coupled) on face '") + - face_name + "'."); + std::string("[read_bc] on face '") + face_name + + "' defines both and . " + "Specify exactly one per face."); } - } else { - // genBC / cplBC / svOneD path. - const bool sv1d_iface = com_mod.cplBC.sv1d_solver_interface.has_data; + if (ci_has_block) { + // 0D face: route to svZeroD. + if (!svzd_iface) { + throw std::runtime_error( + std::string("[read_bc] Face '") + face_name + + "' specifies but no " + "is defined on the equation."); + } + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Coupled)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_Dir)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_Neu)); + lBc.bType = utils::ibclr(lBc.bType, enum_int(BoundaryConditionType::bType_bfs)); + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); + // oned_input_file stays empty — marks this as a 0D face in the + // CoupledBoundaryCondition construction block below. - if (sv1d_iface) { - // For svOneD: each coupled face must specify its own 1D input file via - // ... - if (!ci_set || !bc_params->coupling_interface.svoned_input_file.defined()) { + } else if (ci_has_1d_file) { + // 1D face: route to svOneD. + if (!sv1d_iface) { throw std::runtime_error( - std::string("[read_bc] With , each 1D-coupled face needs " - " with (Time_dependence Coupled) " - "on face '") + - face_name + "'."); + std::string("[read_bc] Face '") + face_name + + "' specifies but no " + "is defined on the equation."); } lBc.oned_input_file = bc_params->coupling_interface.svoned_input_file.value(); @@ -295,26 +302,46 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, } } else { - // genBC / cplBC: uses cplBC.fa array mechanism. + // is present but has neither field, or is absent entirely. if (ci_set) { throw std::runtime_error( - "[read_bc] is only valid when or " - " is defined on the equation."); + std::string("[read_bc] on face '") + face_name + + "' must define either (for 0D coupling) or " + " (for 1D coupling)."); } - - - lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); - com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; - lBc.cplBCptr = com_mod.cplBC.nFa - 1; - - if (com_mod.cplBC.schm == CplBCType::cplBC_NA) { + if (svzd_iface) { throw std::runtime_error( - std::string("[read_bc] A coupling method (e.g. Couple_to_genBC) must be defined for Time_dependence " - "Coupled on face '") + + std::string("[read_bc] With , each svZeroD-coupled face needs " + " with " + "(Time_dependence Coupled) on face '") + + face_name + "'."); + } else { + throw std::runtime_error( + std::string("[read_bc] With , each 1D-coupled face needs " + " with " + "(Time_dependence Coupled) on face '") + face_name + "'."); } } + } else { + // genBC / cplBC path: no svZeroD or svOneD interface defined. + if (bc_params->coupling_interface.value_set) { + throw std::runtime_error( + "[read_bc] is only valid when or " + " is defined on the equation."); + } + + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_cpl)); + com_mod.cplBC.nFa = com_mod.cplBC.nFa + 1; + lBc.cplBCptr = com_mod.cplBC.nFa - 1; + + if (com_mod.cplBC.schm == CplBCType::cplBC_NA) { + throw std::runtime_error( + std::string("[read_bc] A coupling method (e.g. Couple_to_genBC) must be defined for Time_dependence " + "Coupled on face '") + + face_name + "'."); + } } } else if (ctmp == "Resistance") { @@ -444,7 +471,9 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, face_name + "'."); } - if (com_mod.cplBC.sv1d_solver_interface.has_data) { + const bool is_sv1d_face = !lBc.oned_input_file.empty(); + + if (is_sv1d_face) { // ------------------------------------------------------------------ // svOneD path: construct CoupledBoundaryCondition with empty // block_name and store the 1D input file path. @@ -1549,14 +1578,33 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) cplBC.useGenBC = true; cplbc_type_str = eq_params->couple_to_genBC.type.value(); - } else if (eq_params->svzerodsolver_interface_parameters.defined()) { - cplBC.useSvZeroD = true; - cplbc_type_str = eq_params->svzerodsolver_interface_parameters.coupling_type.value(); - cplBC.svzerod_solver_interface.set_data(eq_params->svzerodsolver_interface_parameters); - } else if (eq_params->svonedsolver_interface_parameters.defined()) { - cplBC.useSv1D = true; - cplbc_type_str = eq_params->svonedsolver_interface_parameters.coupling_type.value(); - cplBC.sv1d_solver_interface.set_data(eq_params->svonedsolver_interface_parameters); + } else { + // Allow svZeroDSolver_interface and svOneDSolver_interface to coexist + // (mixed coupling: some faces go to 0D, others to 1D). + // + // Processing order: svZeroD first, then svOneD. + // cplbc_type_str is set by whichever interface is processed first; + // the second interface must match that type. + if (eq_params->svzerodsolver_interface_parameters.defined()) { + cplBC.useSvZeroD = true; + cplbc_type_str = eq_params->svzerodsolver_interface_parameters.coupling_type.value(); + cplBC.svzerod_solver_interface.set_data(eq_params->svzerodsolver_interface_parameters); + } + + if (eq_params->svonedsolver_interface_parameters.defined()) { + const std::string sv1d_type = eq_params->svonedsolver_interface_parameters.coupling_type.value(); + // When both interfaces are defined, their Coupling_type must match. + if (!cplbc_type_str.empty() && cplbc_type_str != sv1d_type) { + throw std::runtime_error( + "[read_eq] svZeroDSolver_interface and svOneDSolver_interface must use the same " + "Coupling_type in a mixed-coupling simulation " + "(svZeroDSolver_interface has '" + cplbc_type_str + + "', svOneDSolver_interface has '" + sv1d_type + "')."); + } + cplbc_type_str = sv1d_type; + cplBC.useSv1D = true; + cplBC.sv1d_solver_interface.set_data(eq_params->svonedsolver_interface_parameters); + } } if (eq_params->couple_to_genBC.defined() || @@ -1578,10 +1626,7 @@ void read_eq(Simulation* simulation, EquationParameters* eq_params, eqType& lEq) cplBC.nX = 0; cplBC.xp.resize(cplBC.nX); - } else if (cplBC.useSvZeroD) { - cplBC.nX = 0; - - } else if (cplBC.useSv1D) { + } else { cplBC.nX = 0; } } diff --git a/Code/Source/solver/set_bc.cpp b/Code/Source/solver/set_bc.cpp index e283762b0..387c7b9d2 100644 --- a/Code/Source/solver/set_bc.cpp +++ b/Code/Source/solver/set_bc.cpp @@ -179,20 +179,21 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& // Call genBC or cplBC to get updated pressures or flowrates. if (cplBC.useGenBC) { set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); - } else if (cplBC.useSvZeroD) { - svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with svZeroD faces. - if (RCRflag) { - set_bc::cplBC_Integ_X(com_mod, cm_mod, true); - } - } else if (cplBC.useSv1D) { - svOneD::calc_svOneD(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with svOneD faces. - if (RCRflag) { - set_bc::cplBC_Integ_X(com_mod, cm_mod, true); - } } else { - set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); + // In mixed-coupling simulations both useSvZeroD and useSv1D can be true. + // Call each active solver independently. + if (cplBC.useSvZeroD) { + svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); + } + if (cplBC.useSv1D) { + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + } + if (!cplBC.useSvZeroD && !cplBC.useSv1D) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); + } else if (RCRflag) { + // Also integrate any RCR faces that coexist with svZeroD/svOneD faces. + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } } // Compute the epsilon parameter (diff) for the finite difference calculation @@ -290,7 +291,7 @@ void calc_der_cpl_bc(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& // Perturb flowrate and compute new pressure bc.coupled_bc.perturb_flowrate(diff); - if (cplBC.useSv1D) { + if (bc.coupled_bc.is_sv1d_face()) { svOneD::calc_svOneD(com_mod, cm_mod, 'D'); } else { svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); @@ -845,20 +846,21 @@ void set_bc_cpl(ComMod& com_mod, CmMod& cm_mod, const SolutionStates& solutions) // Updates pressure or flowrates stored in cplBC.fa[i].y if (cplBC.useGenBC) { set_bc::genBC_Integ_X(com_mod, cm_mod, "D"); - } else if (cplBC.useSvZeroD){ - svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with svZeroD faces. - if (RCRflag) { - set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } else { + // In mixed-coupling simulations both useSvZeroD and useSv1D can be true. + // Call each active solver independently. + if (cplBC.useSvZeroD) { + svZeroD::calc_svZeroD(com_mod, cm_mod, 'D'); } - } else if (cplBC.useSv1D) { - svOneD::calc_svOneD(com_mod, cm_mod, 'D'); - // Also integrate any RCR faces that coexist with svOneD faces. - if (RCRflag) { + if (cplBC.useSv1D) { + svOneD::calc_svOneD(com_mod, cm_mod, 'D'); + } + if (!cplBC.useSvZeroD && !cplBC.useSv1D) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); + } else if (RCRflag) { + // Also integrate any RCR faces that coexist with svZeroD/svOneD faces. set_bc::cplBC_Integ_X(com_mod, cm_mod, true); } - } else { - set_bc::cplBC_Integ_X(com_mod, cm_mod, RCRflag); } } diff --git a/Code/Source/solver/svZeroD_interface.cpp b/Code/Source/solver/svZeroD_interface.cpp index 643b09c26..6d9898736 100644 --- a/Code/Source/solver/svZeroD_interface.cpp +++ b/Code/Source/solver/svZeroD_interface.cpp @@ -27,7 +27,13 @@ static void build_svzero_coupled_bc_idxs(ComMod& com_mod) cpl.svZeroD_coupled_bc_idxs.clear(); for (int iEq = 0; iEq < com_mod.nEq; iEq++) { for (int iBc = 0; iBc < com_mod.eq[iEq].nBc; iBc++) { - if (utils::btest(com_mod.eq[iEq].bc[iBc].bType, consts::iBC_Coupled)) { + const auto& bc = com_mod.eq[iEq].bc[iBc]; + // A Coupled face belongs to svZeroD only when it is not a 1D face. + // The is_sv1d_face() helper encapsulates the routing invariant: + // 1D face → oned_input_file_ non-empty, block_name_ empty + // 0D face → block_name_ non-empty, oned_input_file_ empty + if (utils::btest(bc.bType, consts::iBC_Coupled) && + !bc.coupled_bc.is_sv1d_face()) { cpl.svZeroD_coupled_bc_idxs.emplace_back(iEq, iBc); } } diff --git a/Code/Source/solver/txt.cpp b/Code/Source/solver/txt.cpp index ea637487c..aaa316ebb 100644 --- a/Code/Source/solver/txt.cpp +++ b/Code/Source/solver/txt.cpp @@ -175,54 +175,52 @@ void txt(Simulation* simulation, const bool init_write, const SolutionStates& so if (!init_write) { if (cplBC.useGenBC) { set_bc::genBC_Integ_X(com_mod, cm_mod, "L"); - } else if (cplBC.useSvZeroD) { - svZeroD::calc_svZeroD(com_mod, cm_mod, 'L'); - // Also integrate any RCR faces that coexist with svZeroD faces. - for (auto& bc : com_mod.eq[0].bc) { - if (utils::btest(bc.bType, iBC_RCR)) { - ltmp = true; - break; - } - } - if (ltmp) { - set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } else { + // In mixed-coupling simulations both useSvZeroD and useSv1D can be true. + // Call each active solver independently. + if (cplBC.useSvZeroD) { + svZeroD::calc_svZeroD(com_mod, cm_mod, 'L'); } - } else if (cplBC.useSv1D) { - // Update NEU coupling flowrates from the final converged velocity - // field (Yn) before committing the 1D solution. During the Newton - // loop, compute_flowrates() is called at the *start* of each - // iteration using a partially-converged Yn. After the last picc - // correction Yn is fully converged, but Qn_ is never refreshed. - // Re-computing here ensures the 1D solver receives the same - // flowrate that write_boundary_integral_data() reports in - // B_NS_Velocity_flux.txt. - for (auto& bc : com_mod.eq[0].bc) { - if (utils::btest(bc.bType, iBC_Coupled) && - bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { - bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + if (cplBC.useSv1D) { + // Update NEU coupling flowrates from the final converged velocity + // field (Yn) before committing the 1D solution. During the Newton + // loop, compute_flowrates() is called at the *start* of each + // iteration using a partially-converged Yn. After the last picc + // correction Yn is fully converged, but Qn_ is never refreshed. + // Re-computing here ensures the 1D solver receives the same + // flowrate that write_boundary_integral_data() reports in + // B_NS_Velocity_flux.txt. + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_Coupled) && + bc.coupled_bc.is_sv1d_face() && + bc.coupled_bc.get_bc_type() == BoundaryConditionType::bType_Neu) { + bc.coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + } } - } svOneD::calc_svOneD(com_mod, cm_mod, 'L'); - // Also integrate any RCR faces that coexist with svOneD faces. - for (auto& bc : com_mod.eq[0].bc) { - if (utils::btest(bc.bType, iBC_RCR)) { - ltmp = true; - break; - } - } - if (ltmp) { - set_bc::cplBC_Integ_X(com_mod, cm_mod, true); } - } else { - for (auto& bc : com_mod.eq[0].bc) { - if (utils::btest(bc.bType, iBC_RCR)) { - ltmp = true; - break; + // Also integrate any RCR faces coexisting with svZeroD/svOneD faces. + if (cplBC.useSvZeroD || cplBC.useSv1D) { + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_RCR)) { + ltmp = true; + break; + } + } + if (ltmp) { + set_bc::cplBC_Integ_X(com_mod, cm_mod, true); + } + } else { + for (auto& bc : com_mod.eq[0].bc) { + if (utils::btest(bc.bType, iBC_RCR)) { + ltmp = true; + break; + } } + set_bc::cplBC_Integ_X(com_mod, cm_mod, ltmp); } - set_bc::cplBC_Integ_X(com_mod, cm_mod, ltmp); } } } From ab9872afabd10ea28bfc12b8466125bdea587297 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 4 Jun 2026 14:51:14 -0700 Subject: [PATCH 45/51] fix: remove stale TTPInitialConditionsParameters reference --- Code/Source/solver/Parameters.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index a90fb44a4..2176cf587 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -1405,7 +1405,6 @@ class DomainParameters : public ParameterLists StimulusParameters stimulus; FluidViscosityParameters fluid_viscosity; SolidViscosityParameters solid_viscosity; - TTPInitialConditionsParameters ttp_initial_conditions; // Ionic model parameters. std::map> ionic_models; From 3ed2c50ec528c30bcbba78d2b7684d6b01ef01cc Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 4 Jun 2026 14:55:43 -0700 Subject: [PATCH 46/51] fix: remove stale TTP initial condition parameter code --- Code/Source/solver/Parameters.cpp | 181 ------------------------------ 1 file changed, 181 deletions(-) diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 2434d97c3..b07b6242b 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -1976,187 +1976,6 @@ void DomainParameters::set_values(tinyxml2::XMLElement* domain_elem, bool from_e */ } -////////////////////////////////////////////////////////// -// TTPInitialConditionsParameters // -////////////////////////////////////////////////////////// - -const std::string TTPInitialConditionsParameters::xml_element_name_ = "TTP_initial_conditions"; - -TTPInitialConditionsParameters::TTPInitialConditionsParameters() -{ -} - -void TTPInitialConditionsParameters::print_parameters() -{ - if (value_set) { - std::cout << std::endl; - std::cout << "TTP Initial Conditions Parameters" << std::endl; - std::cout << "---------------------------------" << std::endl; - initial_states.print_parameters(); - gating_variables.print_parameters(); - } -} - -void TTPInitialConditionsParameters::set_values(tinyxml2::XMLElement* xml_elem) -{ - using namespace tinyxml2; - std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - - auto item = xml_elem->FirstChildElement(); - - while (item != nullptr) { - auto name = std::string(item->Value()); - - if (name == TTPInitialStatesParameters::xml_element_name_) { - initial_states.set_values(item); - value_set = true; - - } else if (name == TTPGatingVariablesParameters::xml_element_name_) { - gating_variables.set_values(item); - value_set = true; - - } else { - throw std::runtime_error(error_msg + name + "'."); - } - - item = item->NextSiblingElement(); - } - - if (!initial_states.defined()) { - throw std::runtime_error(xml_element_name_ + " requires an '" + - TTPInitialStatesParameters::xml_element_name_ + "' XML section."); - } - - if (!gating_variables.defined()) { - throw std::runtime_error(xml_element_name_ + " requires a '" + - TTPGatingVariablesParameters::xml_element_name_ + "' XML section."); - } -} - -////////////////////////////////////////////////////////// -// TTPInitialStatesParameters // -////////////////////////////////////////////////////////// - -const std::string TTPInitialStatesParameters::xml_element_name_ = "Initial_states"; - -TTPInitialStatesParameters::TTPInitialStatesParameters() -{ - bool required = true; - - set_parameter("V", -85.23, required, V); - set_parameter("K_i", 136.89, required, K_i); - set_parameter("Na_i", 8.6040, required, Na_i); - set_parameter("Ca_i", 1.26E-4, required, Ca_i); - set_parameter("Ca_ss", 3.6E-4, required, Ca_ss); - set_parameter("Ca_sr", 3.64, required, Ca_sr); - set_parameter("R_bar", 0.9073, required, R_bar); -} - -void TTPInitialStatesParameters::print_parameters() -{ - if (value_set) { - std::cout << " Initial States:" << std::endl; - auto params_name_value = get_parameter_list(); - for (auto& [key, value] : params_name_value) { - std::cout << " " << key << ": " << value << std::endl; - } - } -} - -void TTPInitialStatesParameters::set_values(tinyxml2::XMLElement* xml_elem) -{ - using namespace tinyxml2; - std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - - auto item = xml_elem->FirstChildElement(); - - while (item != nullptr) { - auto name = std::string(item->Value()); - - if (item->GetText() != nullptr) { - auto value = item->GetText(); - try { - set_parameter_value(name, value); - value_set = true; - } catch (const std::bad_function_call& exception) { - throw std::runtime_error(error_msg + name + "'."); - } - } else { - throw std::runtime_error(error_msg + name + "'."); - } - - item = item->NextSiblingElement(); - } - - check_required(); -} - -////////////////////////////////////////////////////////// -// TTPGatingVariablesParameters // -////////////////////////////////////////////////////////// - -const std::string TTPGatingVariablesParameters::xml_element_name_ = "Gating_variables"; - -TTPGatingVariablesParameters::TTPGatingVariablesParameters() -{ - bool required = true; - - set_parameter("x_r1_rectifier", 6.21E-3, required, x_r1_rectifier); - set_parameter("x_r2_rectifier", 0.4712, required, x_r2_rectifier); - set_parameter("x_s_rectifier", 9.5E-3, required, x_s_rectifier); - - set_parameter("m_fast_Na", 1.72E-3, required, m_fast_Na); - set_parameter("h_fast_Na", 0.7444, required, h_fast_Na); - set_parameter("j_fast_Na", 0.7045, required, j_fast_Na); - - set_parameter("d_slow_in", 3.373E-5, required, d_slow_in); - set_parameter("f_slow_in", 0.7888, required, f_slow_in); - set_parameter("f2_slow_in", 0.9755, required, f2_slow_in); - set_parameter("fcass_slow_in", 0.9953, required, fcass_slow_in); - - set_parameter("s_out", 0.999998, required, s_out); - set_parameter("r_out", 2.42E-8, required, r_out); -} - -void TTPGatingVariablesParameters::print_parameters() -{ - if (value_set) { - std::cout << " Gating Variables:" << std::endl; - auto params_name_value = get_parameter_list(); - for (auto& [key, value] : params_name_value) { - std::cout << " " << key << ": " << value << std::endl; - } - } -} - -void TTPGatingVariablesParameters::set_values(tinyxml2::XMLElement* xml_elem) -{ - using namespace tinyxml2; - std::string error_msg = "Unknown " + xml_element_name_ + " XML element '"; - - auto item = xml_elem->FirstChildElement(); - - while (item != nullptr) { - auto name = std::string(item->Value()); - - if (item->GetText() != nullptr) { - auto value = item->GetText(); - try { - set_parameter_value(name, value); - value_set = true; - } catch (const std::bad_function_call& exception) { - throw std::runtime_error(error_msg + name + "'."); - } - } else { - throw std::runtime_error(error_msg + name + "'."); - } - - item = item->NextSiblingElement(); - } - - check_required(); -} - ////////////////////////////////////////////////////////// // DirectionalDistributionParameters // ////////////////////////////////////////////////////////// From 9b8675dae03556518ddae1bbce551a17a5d648ce Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 4 Jun 2026 15:05:36 -0700 Subject: [PATCH 47/51] fix: restore coupled_bc_type initialization after mixed 0D/1D merge --- Code/Source/solver/read_files.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Code/Source/solver/read_files.cpp b/Code/Source/solver/read_files.cpp index c092624d1..e2fc91a7c 100644 --- a/Code/Source/solver/read_files.cpp +++ b/Code/Source/solver/read_files.cpp @@ -128,12 +128,15 @@ void read_bc(Simulation* simulation, EquationParameters* eq_params, eqType& lEq, { using namespace consts; auto bc_type = bc_params->type.value(); + BoundaryConditionType coupled_bc_type = BoundaryConditionType::bType_Neu; if (std::set{"Dirichlet", "Dir"}.count(bc_type)) { - lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Dir)); + lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Dir)); + coupled_bc_type = BoundaryConditionType::bType_Dir; } else if (std::set{"Neumann", "Neu"}.count(bc_type)) { lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_Neu)); + coupled_bc_type = BoundaryConditionType::bType_Neu; if ((lEq.phys == EquationType::phys_fluid) || (lEq.phys == EquationType::phys_FSI)) { lBc.bType = utils::ibset(lBc.bType, enum_int(BoundaryConditionType::bType_bfs)); } From 3cc56374c8cfec95fbe27dae95942eedcdd416d4 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 4 Jun 2026 15:09:24 -0700 Subject: [PATCH 48/51] fix: remove stale static CmMod definition --- Code/Source/solver/CoupledBoundaryCondition.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Code/Source/solver/CoupledBoundaryCondition.cpp b/Code/Source/solver/CoupledBoundaryCondition.cpp index dabd324ca..0f4ce18af 100644 --- a/Code/Source/solver/CoupledBoundaryCondition.cpp +++ b/Code/Source/solver/CoupledBoundaryCondition.cpp @@ -16,8 +16,6 @@ // CoupledBoundaryCondition // ========================================================================= -const CmMod CoupledBoundaryCondition::cm_mod_{}; - CoupledBoundaryCondition::CoupledBoundaryCondition(const CoupledBoundaryCondition& other) : face_(other.face_) , cap_face_vtp_file_(other.cap_face_vtp_file_) From 4b968c1c0475a918298433164bf5ef6cbd2c0bd1 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 4 Jun 2026 15:22:32 -0700 Subject: [PATCH 49/51] remove coupling fom the parameter naming --- Code/Source/solver/Parameters.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index b07b6242b..4b145f80f 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -469,9 +469,9 @@ CouplingInterfaceParameters::CouplingInterfaceParameters() set_parameter("svZeroDSolver_block", "", !required, svzerod_solver_block); set_parameter("Chamber_cap_surface", "", !required, chamber_cap_surface); set_parameter("svOneDSolver_input_file","", !required, svoned_input_file); - set_parameter("Coupling_ramp_steps", 0, !required, coupling_ramp_steps); - set_parameter("Coupling_ramp_ref_pressure", 0.0, !required, coupling_ramp_ref_pressure); - set_parameter("Coupling_relax_factor", 1.0, !required, coupling_relax_factor); + set_parameter("Ramp_steps", 0, !required, coupling_ramp_steps); + set_parameter("Ramp_ref_pressure", 0.0, !required, coupling_ramp_ref_pressure); + set_parameter("Relax_factor", 1.0, !required, coupling_relax_factor); } void CouplingInterfaceParameters::set_values(tinyxml2::XMLElement* xml_elem) From 45572537b602d53067a0bb3d8de583c4f0a58232 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Thu, 21 May 2026 12:27:51 -0700 Subject: [PATCH 50/51] new featuer: read scalar pressure from solver.xml and initialize 3d domain with it. fix: remove coupling_ on parameter names --- Code/Source/solver/ComMod.h | 6 ++++++ Code/Source/solver/Parameters.cpp | 1 + Code/Source/solver/Parameters.h | 2 ++ Code/Source/solver/Simulation.cpp | 6 ++++++ Code/Source/solver/distribute.cpp | 3 +++ Code/Source/solver/initialize.cpp | 15 +++++++++++---- 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Code/Source/solver/ComMod.h b/Code/Source/solver/ComMod.h index 32a18b3af..81ca89a53 100644 --- a/Code/Source/solver/ComMod.h +++ b/Code/Source/solver/ComMod.h @@ -1668,6 +1668,9 @@ class ComMod { bool usePrecomp = false; //----- int members -----// + /// @brief scalar initial pressure is explicitly provided in the input file + bool have_initial_pressure = false; + /// @brief Current domain int cDmn = 0; @@ -1750,6 +1753,9 @@ class ComMod { /// @brief Time double time = 0.0; + /// @brief initial scalar pressure value + double initial_pressure = 0.0; + //----- string members -----// diff --git a/Code/Source/solver/Parameters.cpp b/Code/Source/solver/Parameters.cpp index 4b145f80f..7e910e711 100644 --- a/Code/Source/solver/Parameters.cpp +++ b/Code/Source/solver/Parameters.cpp @@ -2558,6 +2558,7 @@ GeneralSimulationParameters::GeneralSimulationParameters() set_parameter("Include_xml", "", !required, include_xml); set_parameter("Increment_in_saving_restart_files", 0, !required, increment_in_saving_restart_files); set_parameter("Increment_in_saving_VTK_files", 0, !required, increment_in_saving_vtk_files); + set_parameter("Initial_pressure_scalar", 0.0, !required, initial_pressure); set_parameter("Name_prefix_of_saved_VTK_files", "", !required, name_prefix_of_saved_vtk_files); set_parameter("Number_of_initialization_time_steps", 0, !required, number_of_initialization_time_steps, {0,int_inf}); diff --git a/Code/Source/solver/Parameters.h b/Code/Source/solver/Parameters.h index 2176cf587..dfcc8db3a 100644 --- a/Code/Source/solver/Parameters.h +++ b/Code/Source/solver/Parameters.h @@ -1655,6 +1655,8 @@ class GeneralSimulationParameters : public ParameterLists Parameter spectral_radius_of_infinite_time_step; Parameter time_step_size; + Parameter initial_pressure; + Parameter include_xml; Parameter increment_in_saving_restart_files; diff --git a/Code/Source/solver/Simulation.cpp b/Code/Source/solver/Simulation.cpp index 7e6d56523..8cef36530 100644 --- a/Code/Source/solver/Simulation.cpp +++ b/Code/Source/solver/Simulation.cpp @@ -60,6 +60,12 @@ void Simulation::set_module_parameters() com_mod.nITs = general.number_of_initialization_time_steps.value(); com_mod.startTS = general.starting_time_step.value(); com_mod.dt = general.time_step_size.value(); + com_mod.have_initial_pressure = general.initial_pressure.defined(); // Set the flag to indicate whether initial pressure is provided in the input file + if (com_mod.have_initial_pressure) { + com_mod.initial_pressure = general.initial_pressure.value(); + } else { + com_mod.initial_pressure = 0.0; // Default pressure value if not provided + } com_mod.stopTrigName = general.searched_file_name_to_trigger_stop.value(); com_mod.ichckIEN = general.check_ien_order.value(); diff --git a/Code/Source/solver/distribute.cpp b/Code/Source/solver/distribute.cpp index 8adc11a35..32e160e20 100644 --- a/Code/Source/solver/distribute.cpp +++ b/Code/Source/solver/distribute.cpp @@ -331,6 +331,9 @@ void distribute(Simulation* simulation) cm.bcast(cm_mod, &com_mod.nEq); cm.bcast(cm_mod, &com_mod.dt); cm.bcast(cm_mod, &com_mod.precompDt); + cm.bcast(cm_mod, &com_mod.have_initial_pressure); + cm.bcast(cm_mod, &com_mod.initial_pressure); + cm.bcast(cm_mod, &com_mod.zeroAve); cm.bcast(cm_mod, &com_mod.cmmInit); diff --git a/Code/Source/solver/initialize.cpp b/Code/Source/solver/initialize.cpp index de31ec97e..17438db2a 100644 --- a/Code/Source/solver/initialize.cpp +++ b/Code/Source/solver/initialize.cpp @@ -937,15 +937,22 @@ void zero_init(Simulation* simulation, SolutionStates& solutions) } } - if (com_mod.Pinit.size() != 0) { + if (com_mod.Pinit.size() != 0) {// read initial pressure from vtk file. nodewise pressure value #ifdef debug_zero_init dmsg << "Initialize Yo to provided P solution"; #endif for (int a = 0; a < com_mod.tnNo; a++) { - for (int i = 0; i < nsd; i++) { - Yo(nsd,a) = com_mod.Pinit(a); - } + Yo(nsd,a) = com_mod.Pinit(a); + } + } else if (com_mod.have_initial_pressure) {// read initial pressure from solver.xml. scalar initial pressure value + #ifdef debug_zero_init + dmsg << "Initialize Yo to scalar initial pressure from solver.xml"; + #endif + for (int a = 0; a < com_mod.tnNo; a++) { + Yo(nsd,a) = com_mod.initial_pressure; } + std::string msg = "Initialize Yo(nsd,a) to initial_pressure value from solver.xml: " + std::to_string(com_mod.initial_pressure); + std::cout << msg << std::endl; } if (com_mod.Dinit.size() != 0) { From 4a143f1d142be5b557e31be29eb4def24ef78960 Mon Sep 17 00:00:00 2001 From: Taeouk Kim Date: Fri, 29 May 2026 13:22:41 -0700 Subject: [PATCH 51/51] fix: with non-zero initial pressure, under-relaxation of the first time step now uses proper P0. P0 is average pressure of the coupled surface from the inital condition --- Code/Source/solver/baf_ini.cpp | 20 ++++++++------ Code/Source/solver/svOneD_subroutines.cpp | 20 +++++++++++--- Code/Source/solver/svOneD_subroutines.h | 2 +- Code/Source/solver/svZeroD_interface.cpp | 33 +++++++++++++++++++---- Code/Source/solver/svZeroD_interface.h | 2 +- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/Code/Source/solver/baf_ini.cpp b/Code/Source/solver/baf_ini.cpp index 3fd00789c..d96a8f9bd 100644 --- a/Code/Source/solver/baf_ini.cpp +++ b/Code/Source/solver/baf_ini.cpp @@ -154,13 +154,17 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) } } + // Create temporary SolutionStates from initialized 3D state. + SolutionStates init_solutions; + init_solutions.old.get_acceleration() = Ao; + init_solutions.old.get_displacement() = Do; + init_solutions.old.get_velocity() = Yo; + init_solutions.current.get_acceleration() = Ao; + init_solutions.current.get_displacement() = Do; + init_solutions.current.get_velocity() = Yo; + if (!com_mod.stFileFlag) { - // Create temporary SolutionStates for set_bc calls - SolutionStates temp_solutions; - temp_solutions.old.get_acceleration() = Ao; - temp_solutions.old.get_displacement() = Do; - temp_solutions.old.get_velocity() = Yo; - set_bc::rcr_init(com_mod, cm_mod, temp_solutions); + set_bc::rcr_init(com_mod, cm_mod, init_solutions); } if (com_mod.cplBC.useGenBC) { @@ -168,11 +172,11 @@ void baf_ini(Simulation* simulation, SolutionStates& solutions) } if (com_mod.cplBC.useSvZeroD) { - svZeroD::init_svZeroD(com_mod, cm_mod); + svZeroD::init_svZeroD(com_mod, cm_mod, init_solutions); } if (com_mod.cplBC.useSv1D) { - svOneD::init_svOneD(com_mod, cm_mod); + svOneD::init_svOneD(com_mod, cm_mod, init_solutions); } // Initialize cap integration for Coupled boundary conditions diff --git a/Code/Source/solver/svOneD_subroutines.cpp b/Code/Source/solver/svOneD_subroutines.cpp index 8789617a8..eed2e7932 100644 --- a/Code/Source/solver/svOneD_subroutines.cpp +++ b/Code/Source/solver/svOneD_subroutines.cpp @@ -63,6 +63,7 @@ #include #include "ComMod.h" +#include "all_fun.h" #include "consts.h" #include "utils.h" #include "svOneD_interface/OneDSolverInterface.h" @@ -157,7 +158,7 @@ static std::string resolve_lib_path(const std::string& lib_base) // --------------------------------------------------------------------------- // init_svOneD // --------------------------------------------------------------------------- -void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) +void init_svOneD(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& solutions) { using namespace consts; @@ -192,6 +193,19 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) st.ramp_steps = bc.coupled_bc.get_oned_ramp_steps(); st.ramp_ref_pressure = bc.coupled_bc.get_oned_ramp_ref_pressure(); st.relax_factor = bc.coupled_bc.get_oned_relax_factor(); + + const auto& face = com_mod.msh[bc.iM].fa[bc.iFa]; + const double area = face.area; + if (area > 0.0) { + const auto& Yo = solutions.old.get_velocity(); + const double P_init = all_fun::integ(com_mod, cm_mod, face, Yo, com_mod.nsd, + solutions, std::nullopt, false, + MechanicalConfigurationType::reference) / area; + st.P_prev_sent_old = P_init; + st.P_prev_sent_new = P_init; + st.P_neu_prev = P_init; + } + oned_models.push_back(std::move(st)); } } @@ -250,8 +264,8 @@ void init_svOneD(ComMod& com_mod, const CmMod& cm_mod) st.solution.resize(system_size, 0.0); shared_lib_instance->return_solution(problem_id, st.solution.data(), system_size); - // Initial coupled value = 0; first calc_svOneD call sets the real value. - eq.bc[st.iBc].coupled_bc.set_pressure(0.0); + // Keep the BC pressure state consistent with the seeded pressure history. + eq.bc[st.iBc].coupled_bc.set_pressure(st.P_neu_prev); } // ----- Broadcast metadata for all models (Phase 2: batch exchange) ----- diff --git a/Code/Source/solver/svOneD_subroutines.h b/Code/Source/solver/svOneD_subroutines.h index 6b8009cd0..a18bc8596 100644 --- a/Code/Source/solver/svOneD_subroutines.h +++ b/Code/Source/solver/svOneD_subroutines.h @@ -12,7 +12,7 @@ namespace svOneD { /// @brief Initialize the 1D solver and populate the initial cplBC state. /// Called once from baf_ini() after the BC data structures are set up. -void init_svOneD(ComMod& com_mod, const CmMod& cm_mod); +void init_svOneD(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& solutions); /// @brief Advance the 1D solver by one time step and update the coupled BC value. /// diff --git a/Code/Source/solver/svZeroD_interface.cpp b/Code/Source/solver/svZeroD_interface.cpp index 6d9898736..d3c5c6c39 100644 --- a/Code/Source/solver/svZeroD_interface.cpp +++ b/Code/Source/solver/svZeroD_interface.cpp @@ -226,7 +226,7 @@ void print_svZeroD(int* nSrfs, const std::vector& surfID, double Q[], doubl // init_svZeroD //-------------- // -void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) +void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& solutions) { using namespace consts; @@ -258,6 +258,11 @@ void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) int ids[2]; double in_out; + const bool have_init_flow = solver_interface.have_initial_flows; + const double init_flow = solver_interface.initial_flows; + const bool have_init_press = solver_interface.have_initial_pressures; + const double init_press = solver_interface.initial_pressures; + if (cm.mas(cm_mod)) { nsrflistCoupled.clear(); svzd_blk_names.clear(); @@ -271,6 +276,7 @@ void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) throw std::runtime_error("ERROR: [init_svZeroD] svZeroDSolver interface data is missing."); } + #ifdef debug_init_svZeroD dmsg << "#### Use XML data #### "; #endif @@ -278,10 +284,6 @@ void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) svzerod_library = solver_interface.solver_library; svzerod_file = solver_interface.configuration_file; - const bool have_init_flow = solver_interface.have_initial_flows; - const double init_flow = solver_interface.initial_flows; - const bool have_init_press = solver_interface.have_initial_pressures; - const double init_press = solver_interface.initial_pressures; for (int s = 0; s < numCoupledSrfs; ++s) { bcType* bc = nullptr; @@ -330,6 +332,7 @@ void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) if (!nth_coupled_bc(com_mod, s, &bc)) { throw std::runtime_error("ERROR: [init_svZeroD] Internal error resolving Coupled BC for initialization."); } + if (bc->coupled_bc.get_bc_type() == consts::BoundaryConditionType::bType_Neu) { if (have_init_flow) { lpn_state_y[sol_IDs[2 * s]] = init_flow; @@ -359,6 +362,26 @@ void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod) } } + for (int s = 0; s < numCoupledSrfs; ++s) { + bcType* bc = nullptr; + if (!nth_coupled_bc(com_mod, s, &bc)) { + throw std::runtime_error("ERROR: [init_svZeroD] Internal error resolving Coupled BC for initialization history."); + } + + // For cap coupling in parallel, flow integration requires all ranks. + if (cm.seq() || !bc->coupled_bc.has_cap()) { + bc->coupled_bc.compute_flowrates(com_mod, cm_mod, solutions); + } + bc->coupled_bc.compute_pressures(com_mod, cm_mod, solutions); + const double Q_init = bc->coupled_bc.get_Qo(); + const double P_init = bc->coupled_bc.get_Po(); + bc->coupled_bc.set_Q_prev_sent(Q_init); + bc->coupled_bc.set_Q_input_prev(Q_init, Q_init); + bc->coupled_bc.set_P_prev_sent(P_init, P_init); + bc->coupled_bc.set_P_neu_prev(P_init); + bc->coupled_bc.set_pressure(P_init); + } + // Broadcast initial values to follower processes if (!cm.seq()) { // Coupled BCs - broadcast Neumann pressures (one scalar bcast per BC). diff --git a/Code/Source/solver/svZeroD_interface.h b/Code/Source/solver/svZeroD_interface.h index 1de624f85..d807891ee 100644 --- a/Code/Source/solver/svZeroD_interface.h +++ b/Code/Source/solver/svZeroD_interface.h @@ -17,7 +17,7 @@ void get_coupled_QP(ComMod& com_mod, double QCoupled[], double QnCoupled[], doub void print_svZeroD(int* nSrfs, const std::vector& surfID, double Q[], double P[]); -void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod); +void init_svZeroD(ComMod& com_mod, const CmMod& cm_mod, const SolutionStates& solutions); void calc_svZeroD(ComMod& com_mod, const CmMod& cm_mod, char BCFlag);