From 9ac7c20cd007ebfa74797e378e699578a8584f4b Mon Sep 17 00:00:00 2001 From: SKlapproth Date: Tue, 21 Jul 2026 14:13:06 +0200 Subject: [PATCH 01/13] VIZ: ParticleBase, public EnableIDs, const getAttribute --- src/Particle/ParticleBase.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Particle/ParticleBase.h b/src/Particle/ParticleBase.h index 9793da2a9..103a32761 100644 --- a/src/Particle/ParticleBase.h +++ b/src/Particle/ParticleBase.h @@ -85,9 +85,9 @@ namespace ippl { */ template class ParticleBase : public ParticleBaseBase { + public: constexpr static bool EnableIDs = sizeof...(IDProperties) > 0; - public: using vector_type = typename PLayout::vector_type; using index_type = typename PLayout::index_type; using particle_position_type = typename PLayout::particle_position_type; @@ -200,6 +200,12 @@ namespace ippl { return attributes_m.template get()[i]; } + /*! Const overload — needed when called on a const ParticleBase reference. */ + template + const attribute_type* getAttribute(size_t i) const { + return attributes_m.template get()[i]; + } + /*! * Calls a given function for all attributes in the bunch * @tparam MemorySpace the memory space of the attributes to visit (void to visit all of From e031f30b5b980717bdfdb9c015b370ae3f5d53bb Mon Sep 17 00:00:00 2001 From: SKlapproth Date: Tue, 21 Jul 2026 14:20:40 +0200 Subject: [PATCH 02/13] VIZ: Add is_vector_v for ippl vectors --- src/Types/Vector.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Types/Vector.hpp b/src/Types/Vector.hpp index fe6114844..c11e7729b 100644 --- a/src/Types/Vector.hpp +++ b/src/Types/Vector.hpp @@ -8,6 +8,16 @@ #include namespace ippl { + +template +struct is_vector : std::false_type {}; + +template +struct is_vector> : std::true_type {}; + +template +inline constexpr bool is_vector_v = is_vector::type>::value; + namespace detail { template struct isExpression> : std::true_type {}; From e4f93fa47d924fe57cfa594d5ebe68c1a58eb76d Mon Sep 17 00:00:00 2001 From: SKlapproth Date: Tue, 21 Jul 2026 14:33:44 +0200 Subject: [PATCH 03/13] VIZ: Stream/Registry and Stream/InSitu --- CMakeLists.txt | 2 + src/CMakeLists.txt | 72 + src/Particle/ParticleAttrib.h | 11 + src/Particle/ParticleAttrib.hpp | 81 ++ src/Particle/ParticleAttribBase.h | 16 + src/Stream/CMakeLists.txt | 5 + src/Stream/InSitu/CMakeLists.txt | 25 + src/Stream/InSitu/CatalystAdaptor.h | 703 ++++++++++ src/Stream/InSitu/CatalystAdaptor.hpp | 1158 +++++++++++++++++ src/Stream/InSitu/CatalystAdaptorSteering.hpp | 934 +++++++++++++ src/Stream/InSitu/CatalystVisitors.h | 247 ++++ src/Stream/InSitu/ProxyWriter.cpp | 1154 ++++++++++++++++ src/Stream/InSitu/ProxyWriter.h | 273 ++++ src/Stream/InSitu/ProxyWriter.hpp | 115 ++ src/Stream/InSitu/catalyst_scripts/__init__py | 0 .../catalyst_scripts/catalystSubroutines.py | 331 +++++ .../catalyst_extractors/__init__py | 0 .../catalyst_extractors/png_ext_empty.py | 15 + .../catalyst_extractors/png_ext_particle.py | 382 ++++++ .../catalyst_extractors/png_ext_sfield.py | 471 +++++++ .../catalyst_extractors/png_ext_vfield.py | 259 ++++ .../catalyst_scripts/pipeline_default.py | 686 ++++++++++ .../proxy_default_config.yaml | 25 + src/Stream/Registry/CMakeLists.txt | 20 + src/Stream/Registry/RegistryHelper.h | 63 + src/Stream/Registry/ViewRegistry.h | 108 ++ src/Stream/Registry/ViewRegistry.hpp | 41 + src/Stream/Registry/VisRegistryRuntime.h | 230 ++++ src/Stream/Registry/VisRegistryRuntime.hpp | 189 +++ 29 files changed, 7616 insertions(+) create mode 100644 src/Stream/CMakeLists.txt create mode 100644 src/Stream/InSitu/CMakeLists.txt create mode 100644 src/Stream/InSitu/CatalystAdaptor.h create mode 100644 src/Stream/InSitu/CatalystAdaptor.hpp create mode 100644 src/Stream/InSitu/CatalystAdaptorSteering.hpp create mode 100644 src/Stream/InSitu/CatalystVisitors.h create mode 100644 src/Stream/InSitu/ProxyWriter.cpp create mode 100644 src/Stream/InSitu/ProxyWriter.h create mode 100644 src/Stream/InSitu/ProxyWriter.hpp create mode 100644 src/Stream/InSitu/catalyst_scripts/__init__py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalystSubroutines.py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalyst_extractors/__init__py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalyst_extractors/png_ext_empty.py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalyst_extractors/png_ext_particle.py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalyst_extractors/png_ext_sfield.py create mode 100644 src/Stream/InSitu/catalyst_scripts/catalyst_extractors/png_ext_vfield.py create mode 100644 src/Stream/InSitu/catalyst_scripts/pipeline_default.py create mode 100644 src/Stream/InSitu/catalyst_scripts/proxy_default_config.yaml create mode 100644 src/Stream/Registry/CMakeLists.txt create mode 100644 src/Stream/Registry/RegistryHelper.h create mode 100644 src/Stream/Registry/ViewRegistry.h create mode 100644 src/Stream/Registry/ViewRegistry.hpp create mode 100644 src/Stream/Registry/VisRegistryRuntime.h create mode 100644 src/Stream/Registry/VisRegistryRuntime.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9934a1af7..f324062fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ option(IPPL_MARK_FAILING_TESTS OFF) option(IPPL_ENABLE_SCRIPTS "Generate job script templates for some benchmarks/tests" OFF) + +option(IPPL_ENABLE_CATALYST "Enable ParaView Catalyst" OFF) # "Build IPPL as a shared library (ON) or static library (OFF)" OFF) if(IPPL_DYL) # set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) message(WARNING "IPPL_DYL is deprecated; use # -DBUILD_SHARED_LIBS=ON instead.") endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1a5fbab6a..98421b7ee 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -100,6 +100,78 @@ if(IPPL_ENABLE_SOLVERS) add_subdirectory(LinearSolvers) endif() + +message (STATUS "Adding Stream subdirectory") +add_subdirectory (Stream) + +if (IPPL_ENABLE_CATALYST) + message (STATUS "CATALYST enabled") + # Create a list to hold all hint paths + set(CATALYST_HINTS "") + # If a path is provided from the command line, add it to the list first + if(DEFINED CATALYST_HINT_PATH AND EXISTS "${CATALYST_HINT_PATH}") + message(STATUS "Using user-provided Catalyst hint path: ${CATALYST_HINT_PATH}") + list(APPEND CATALYST_HINTS "${CATALYST_HINT_PATH}") + endif() + + # Use the list of hints in find_package + find_package(catalyst REQUIRED + HINTS + ${CATALYST_HINTS} + ) + message (STATUS "Found catalyst_DIR: ${catalyst_DIR}") + if (catalyst_FOUND) + message (STATUS "Found Catalyst:") + target_compile_definitions(ippl PUBLIC + IPPL_ENABLE_CATALYST + ) + + message(STATUS "CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}") + target_compile_definitions(ippl PUBLIC + CATALYST_ADAPTOR_ABS_DIR="${CMAKE_CURRENT_SOURCE_DIR}" + ) + + target_link_libraries(ippl PUBLIC + catalyst::catalyst + ) + # ====================================================== + # ========= CATALYST INFO SUMMARY ====================== + # ====================================================== + + message(STATUS "Catalyst Summary:") + message(STATUS " Found: ${catalyst_FOUND}") + message(STATUS " Version: ${catalyst_VERSION}") + + # Display debug library location + get_property(_cfgs TARGET catalyst::catalyst PROPERTY IMPORTED_CONFIGURATIONS) + foreach(_cfg IN LISTS _cfgs) + get_target_property(_loc catalyst::catalyst IMPORTED_LOCATION_${_cfg}) + if(_loc) + message(STATUS " Library (${_cfg}): ${_loc}") + endif() + endforeach() + + # Headers information + message(STATUS " Headers:") + get_target_property(_inc catalyst::catalyst_headers INTERFACE_INCLUDE_DIRECTORIES) + if(_inc) + message(STATUS " Include directories: ${_inc}") + endif() + + # Config location + if(catalyst_DIR) + message(STATUS " CMake config dir: ${catalyst_DIR}") + endif() + + # Target structure summary + message(STATUS " Target structure:") + message(STATUS " catalyst::catalyst (${_cat_type})") + message(STATUS " catalyst::catalyst_headers (INTERFACE_LIBRARY)") + + endif () +endif() + + include(${PROJECT_SOURCE_DIR}/cmake/PlatformOptions.cmake) target_link_libraries(ippl PUBLIC Kokkos::kokkos MPI::MPI_CXX) diff --git a/src/Particle/ParticleAttrib.h b/src/Particle/ParticleAttrib.h index bbdce337a..6631e4ab5 100644 --- a/src/Particle/ParticleAttrib.h +++ b/src/Particle/ParticleAttrib.h @@ -257,6 +257,17 @@ namespace ippl { */ void internalCopy(const hash_type& indices) override; + #ifdef IPPL_ENABLE_CATALYST + void signConduitBlueprintNode( + const size_type Np_local + , conduit_cpp::Node& node_fields + , ViewRegistry& viewRegistry + , Inform& ca_m + , Inform& ca_warn + , const bool forceHostCopy + ) const override ; + #endif + private: view_type dview_m{"ParticleAttrib::dview", 0}; view_type buf_m{"ParticleAttrib::buf", 0}; diff --git a/src/Particle/ParticleAttrib.hpp b/src/Particle/ParticleAttrib.hpp index 555074c1d..03250596c 100644 --- a/src/Particle/ParticleAttrib.hpp +++ b/src/Particle/ParticleAttrib.hpp @@ -375,4 +375,85 @@ namespace ippl { DefineParticleReduction(Min, min, if (myVal < valL) valL = myVal, std::less) DefineParticleReduction(Prod, prod, valL *= myVal, std::multiplies) + + #ifdef IPPL_ENABLE_CATALYST + ////////////////////////////////////////////////////////////////////////////////////// + // Note: + // In general, for runtime performance, neither function overloading nor if + // constexpr has an inherent advantage when used correctly for compile-time + // dispatch. . + // Function overloading with template parameter extraction or constraints or sfinae + // are all hard to implement in this case, so we switched to if const expr. + ////////////////////////////////////////////////////////////////////////////////////// + template + void ParticleAttrib::signConduitBlueprintNode( + const size_type Np_local, + conduit_cpp::Node& node_fields, + ViewRegistry& viewRegistry, + Inform& ca_m, + Inform& ca_warn, + const bool forceHostCopy + ) const + { + host_mirror_type hostMirror; + if(forceHostCopy){ + hostMirror = this->getHostMirror(); + Kokkos::deep_copy(hostMirror , this->getView()); + } else{ + hostMirror = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), this->getView()); + } + auto field = node_fields[this->name_m]; + field["association"].set_string("vertex"); + field["topology"].set_string("p_unstructured_topo"); + field["volume_dependent"].set_string("false"); + + + if constexpr (std::is_scalar_v) { + // --- SCALAR CASE --- + ca_m << level4 <<"::Execute()excute_entry() for attribute: "<name_m << endl + << " call to:" << endl + << " ParticleAttribute<" << typeid(T).name() << ">::signConduitBlueprintNode()" << endl; + + field["values"].set_external(hostMirror.data(), Np_local); + + + } else if constexpr (is_vector_v) { + // --- VECTOR CASE --- + ca_m << level4 <<"::Execute()excute_entry() for attribute: "<name_m << endl + << " call to:" << endl + << " ParticleAttribute>::signConduitBlueprintNode()" << endl; + + + using elem_t = std::remove_pointer_t; + const size_t stride_bytes = sizeof(elem_t); + // static constexpr size_t stride_bytes = sizeof(elem_t); + + if(Np_local>0){ + field["values/x"].set_external(&hostMirror.data()[0][0], Np_local, 0 , stride_bytes ); + if constexpr (T::dim>=2){ + field["values/y"].set_external(&hostMirror.data()[0][1], Np_local, 0 , stride_bytes ); + } + if constexpr (T::dim>=3) { + field["values/z"].set_external(&hostMirror.data()[0][2], Np_local, 0 , stride_bytes ); + } + }else /* (Np_local=0) */ { + // If Np_local is 0. We MUST provide valid, empty arrays for the gather to work. + using component_type = typename T::value_type; + field["values/x"].set_external(static_cast(nullptr), 0); + if constexpr (T::dim>=2) field["values/y"].set_external(static_cast(nullptr), 0); + if constexpr (T::dim>=3) field["values/z"].set_external(static_cast(nullptr), 0); + } + } else { + // --- INVALID CASE --- + ca_warn << "::Execute()excute_entry() for attribute:"<name_m << endl + << " call to:" << endl + << " ParticleAttribute<" << typeid(T).name() << ">::signConduitBlueprintNode()" << endl + << " For this type of Attribute the Conduit Blueprint description wasnt \n" + << " implemented in ippl. Therefore this type of attribute is not \n" + << " supported for visualisation." << endl; + } + viewRegistry.set(hostMirror); + } + #endif + } // namespace ippl diff --git a/src/Particle/ParticleAttribBase.h b/src/Particle/ParticleAttribBase.h index 3e9baa197..761fe69a2 100644 --- a/src/Particle/ParticleAttribBase.h +++ b/src/Particle/ParticleAttribBase.h @@ -21,6 +21,12 @@ #include "Communicate/Archive.h" + + #ifdef IPPL_ENABLE_CATALYST + #include + #include "Stream/Registry/ViewRegistry.h" + #endif + namespace ippl { namespace detail { // Maximum length for attribute names (including null terminator) @@ -94,6 +100,16 @@ namespace ippl { virtual void applyPermutation(const hash_type&) = 0; virtual void internalCopy(const hash_type&) = 0; + #ifdef IPPL_ENABLE_CATALYST + virtual void signConduitBlueprintNode( + const size_type Np_local + , conduit_cpp::Node& node_fields + , ViewRegistry& viewRegistry + , Inform& ca_m + , Inform& ca_warn + , const bool forceHostCopy + ) const = 0; + #endif protected: const size_type* localNum_mp; char name_m[ATTRIB_NAME_MAX_LEN]; diff --git a/src/Stream/CMakeLists.txt b/src/Stream/CMakeLists.txt new file mode 100644 index 000000000..7d5cc1c07 --- /dev/null +++ b/src/Stream/CMakeLists.txt @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# src/Stream/CMakeLists.txt +# ----------------------------------------------------------------------------- +add_subdirectory(InSitu) +add_subdirectory(Registry) \ No newline at end of file diff --git a/src/Stream/InSitu/CMakeLists.txt b/src/Stream/InSitu/CMakeLists.txt new file mode 100644 index 000000000..8ab0763f6 --- /dev/null +++ b/src/Stream/InSitu/CMakeLists.txt @@ -0,0 +1,25 @@ +# ----------------------------------------------------------------------------- +# src/Stream/InSitu/CMakeLists.txt +# ----------------------------------------------------------------------------- +target_include_directories(ippl + PUBLIC + $ + $ +) + +# Add implementation sources for InSitu components +target_sources(ippl + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/ProxyWriter.cpp +) + + +install(FILES + ProxyWriter.h + ProxyWriter.hpp + CatalystAdaptor.h + CatalystAdaptorSteering.h + CatalystAdaptor.hpp + CatalystVisitors.h + DESTINATION include/Stream/InSitu +) \ No newline at end of file diff --git a/src/Stream/InSitu/CatalystAdaptor.h b/src/Stream/InSitu/CatalystAdaptor.h new file mode 100644 index 000000000..e936e02b8 --- /dev/null +++ b/src/Stream/InSitu/CatalystAdaptor.h @@ -0,0 +1,703 @@ +/** + * @file CatalystAdaptor.h + * @brief Declarations and lightweight types for ParaView Catalyst in-situ integration. + * + * This header provides helper types, forward declarations, and includes used by the + * Catalyst adaptor. Heavy implementation details live in `CatalystAdaptor.hpp` and + * `CatalystAdaptorSteering.hpp`. + */ +#ifndef CatalystAdaptor_h +#define CatalystAdaptor_h + +#include "Ippl.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(MPI_VERSION) +#include +#endif + +#include "Utility/IpplException.h" + +#include "Stream/Registry/ViewRegistry.h" + +#include "Stream/InSitu/ProxyWriter.h" +#include "Stream/Registry/RegistryHelper.h" + + +namespace ippl{ + +/* FORWARD DECLARATION */ +class VisRegistryRuntime; + + + +/** + * @struct Button + * @brief Momentary-action control for steering. + * + * A Button acts like an edge-trigger: it reports a single transition when + * pressed and then resets. Useful for one-shot actions triggered from the + * Catalyst GUI without latching state. + */ +struct Button { + Button() = default; + + // explicit not explicit allows: if(my_btn) to work + operator bool() const { return value_m; } + + // explicit Button(bool v) : value_m(v) {} + explicit Button(bool v){ + + if(v){ // Button is initialized pressd + value_m = true; + priorState_m=false; + } + else /* if(!v) */{ // Button is initialized unpressd + value_m=false; + priorState_m = false; + } + + } + + + // Assignment operator: Button = bool + Button& operator=(bool v) { + if(v) { // Button is being pressed + if(!value_m && !priorState_m) { // True unpressed state + value_m = true; + priorState_m = false; // Fixed typo: was "priot_state" + } + else if(value_m && !priorState_m) { // Button was pressed last iteration and is still "being pushed down" + // Internal button needs to snap back! And mark it as "freshly snapped" + value_m = false; + priorState_m = true; + } + else if(!value_m && priorState_m) { // Button was pressed at some previous iteration and is still "being pushed down" + // Button needs to stay in freshly snapped back state + value_m = false; + priorState_m = true; + } + else if(value_m && priorState_m) { // Impossible state + throw IpplException("CatalystAdaptor::Button Assignment", "Impossible State: Button is malfunctioning!!!"); + } + } + else { // Button is unpressed + value_m = false; + priorState_m = false; + } + return *this; // CRITICAL: Return reference to this object for chaining + } + + // Friend function to overload << operator for output streams + friend std::ostream& operator<<(std::ostream& os, const Button& btn) { + os << (btn.value_m ? "PUSHED" : "not PUSHED"); + return os; + } + + private: + bool value_m = false; + bool priorState_m = false; +};// Button + + +/** + * @brief Host-side 1D mask view (0/1) used for ghost/halo flags. + */ +using HostMaskView1D_t = Kokkos::View; + + +/** + * @brief Cache key for ghosted data. + * + * Tuple components: + * - pointer identifying the mesh/topology + * - pointer identifying the owning view/container + * - size/extent of the ghost region + */ +using GhostKey_t = std::tuple; + + +/** + * @brief Hash functor for GhostKey_t used in unordered caches. + */ +struct GhostKeyHash { + std::size_t operator()(const GhostKey_t& k) const { + // Get the hash for each element in the tuple + auto h1 = std::hash{}(std::get<0>(k)); + auto h2 = std::hash{}(std::get<1>(k)); + auto h3 = std::hash{}(std::get<2>(k)); + + // Combine the hashes. This is a common pattern (based on boost::hash_combine) + // It xors and bit-shifts to mix the bits well. + h1 ^= h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2); + h1 ^= h3 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2); + return h1; + } +}; + + +/** + * @class CatalystAdaptor + * @brief High-level orchestrator for Catalyst initialization, execution, and steering. + * + * The adaptor wires up registered fields/particles and steerables to Conduit `node_m` instances, + * invokes Catalyst scripts, and forwards/fetches steering values between the + * simulation and the GUI. Use the runtime registry API (Initialize/Execute) + * for flexible, non-templated integration. + */ +class CatalystAdaptor { +public: + struct InitVisitor; + struct ExecVisitor; + struct SteerInitVisitor; + struct SteerForwardVisitor; + struct SteerFetchVisitor; + + using VisVisitorVariant_t = std::variant; + using SteerVisitorVariant_t = std::variant; + +private: + std::shared_ptr visRegistry_m; + std::shared_ptr steerRegistry_m; + + ViewRegistry viewRegistry_m; + conduit_cpp::Node node_m; + conduit_cpp::Node results_m; + public: + Inform catalystInfo_m; + Inform catalystWarn_m; + private: + + ProxyWriter proxyWriter_m; + std::unordered_map>> enumChoicesByType_m; + std::unordered_map>> enumChoices_m; + + + const char* catalystVis_m ; + const char* catalystLive_m ; + const char* catalystSteer_m; + const char* catalystPng_m ; + const char* catalystVtk_m ; + const char* catalystVerbosity_m; + const char* catalystGhostMask_m ; + const char* proxyOption_m; + + const bool visEnabled_m; + const bool liveEnabled_m; + const bool steerEnabled_m; + const bool pngExtracts_m ; + const bool vtkExtracts_m ; + const int outputLevel_m ; + const bool useGhostMasks_m; + + // std::string associate_m; + const std::filesystem::path sourceDir_m; + + std::unordered_map forceHostCopy_m; + + std::unordered_map ghostMaskCache_m; + +public: + + + + + CatalystAdaptor() : + catalystInfo_m("CatalystAdaptor::", 0), // Only print on rank 0 + catalystWarn_m("CatalystAdaptor_WARNING", std::cerr, INFORM_ALL_NODES), + catalystVis_m(std::getenv("IPPL_CATALYST_VIS")), + catalystLive_m(std::getenv("IPPL_CATALYST_LIVE")), + catalystSteer_m(std::getenv("IPPL_CATALYST_STEER")), + catalystPng_m(std::getenv("IPPL_CATALYST_PNG")), + catalystVtk_m(std::getenv("IPPL_CATALYST_VTK")), + catalystVerbosity_m(std::getenv("IPPL_CATALYST_VERBOSITY")), + catalystGhostMask_m(std::getenv("IPPL_CATALYST_GHOST_MASKS")), + proxyOption_m(std::getenv("IPPL_CATALYST_PROXY_OPTION")), + visEnabled_m( ! (catalystVis_m && std::string(catalystVis_m) == "OFF") ), + liveEnabled_m( catalystLive_m && std::string(catalystLive_m) == "ON"), + steerEnabled_m( catalystSteer_m && std::string(catalystSteer_m) == "ON"), + pngExtracts_m( catalystPng_m && std::string(catalystPng_m) == "ON"), + vtkExtracts_m( catalystVtk_m && std::string(catalystVtk_m) == "ON"), + outputLevel_m( catalystVerbosity_m ? std::stoi(catalystVerbosity_m) : ippl::Info->getOutputLevel()), + useGhostMasks_m(catalystGhostMask_m && std::string(catalystGhostMask_m) == "ON"), + sourceDir_m(std::filesystem::path(CATALYST_ADAPTOR_ABS_DIR) / "Stream" / "InSitu") + { + // associate_m="element"; + + if(!liveEnabled_m){ + catalystLive_m ="OFF"; + } + + catalystInfo_m.setOutputLevel(outputLevel_m); + + // #if defined(MPI_VERSION) + // MPI_Barrier(MPI_COMM_WORLD); + // if(ippl::Comm->rank()==0) catalystWarn_m << "[rank = 0 size=" << ippl::Comm->size() << "]" << endl; + // MPI_Barrier(MPI_COMM_WORLD); + // if(ippl::Comm->rank()==1) catalystWarn_m << "[rank= 1 size=" << ippl::Comm->size() << "]" << endl; + // MPI_Barrier(MPI_COMM_WORLD); + // #endif + + catalystInfo_m << "::CatalystAdaptor() Global Output Level setting: " << ippl::Info->getOutputLevel() << endl; + catalystInfo_m << "::CatalystAdaptor() Catalyst Info Output Level setting: " << catalystInfo_m.getOutputLevel() << endl; + catalystInfo_m << "::CatalystAdaptor() Catalyst Warn Output Level setting: " << catalystWarn_m.getOutputLevel() << endl; + catalystInfo_m << "::CatalystAdaptor() using sourceDir_m = " << sourceDir_m.string() << endl; + if (pngExtracts_m) + { catalystInfo_m << "::CatalystAdaptor() PNG extraction ACTIVATED" << endl;} + else{ catalystInfo_m << "::CatalystAdaptor() PNG extraction DEACTIVATED" << endl;} + if (vtkExtracts_m) + { catalystInfo_m << "::CatalystAdaptor() VTK extraction ACTIVATED" << endl;} + else{ catalystInfo_m << "::CatalystAdaptor() VTK extraction DEACTIVATED" << endl;} + if (steerEnabled_m) + { catalystInfo_m << "::CatalystAdaptor() Steering ACTIVATED" << endl;} + else{ catalystInfo_m << "::CatalystAdaptor() Steering DEACTIVATED" << endl;} + + } + + private: + + // ============================================================================================== + // HELPERS ===================================================================================== + // ============================================================================================== + /** + * @brief Sets a file path to a node, using an environment variable if available, otherwise a default path. + * + * @param nodePath The Conduit `node_m` instance to set the file path in. + * @param envVar The name of the environment variable to check. + * @param defaultFilePath The default file path to use if the environment variable is not set or invalid. + */ + void setNodeScript( + conduit_cpp::Node nodePath, + // const char* envVar, + const std::string envVar, + const std::filesystem::path defaultFilePath + ); + + + // ========================================================== + // VISUALIZATION CHANNEL INITIALIZERS ======================= + // ========================================================== + + + + /* SCALAR FIELDS - handles both reference and shared_ptr */ + // == ippl::Field, Cell>* + /** + * @brief Initializes a Conduit `node_m` instance entry for a scalar field. + * + * @tparam T Field value type. + * @tparam Dim Field dimension. + * @tparam ViewArgs Additional template arguments for the field. + * @param entry The scalar field to initialize. + * @param label The label for the field/channel. + */ + template + void InitVizChannel( + [[maybe_unused]] + const Field& entry + , const std::string label + ); + + + /* VECTOR FIELDS - handles both reference and shared_ptr */ + // == ippl::Field, 3, ippl::UniformCartesian, Cell>* + /** + * @brief Initializes a Conduit `node_m` instance entry for a vector field. + * + * @tparam T Vector value type. + * @tparam Dim Field dimension. + * @tparam Dim_v Vector dimension. + * @tparam ViewArgs Additional template arguments for the field. + * @param entry The vector field to initialize. + * @param label The label for the field/channel. + */ + template + void InitVizChannel( + [[maybe_unused]] + const Field, Dim, ViewArgs...>& entry + , const std::string label + ); + + + + // PARTICLECONTAINERS DERIVED FROM PARTICLEBASE: + // == ippl::ParticleBaseBase -> ParticleBase, ... , ... > + /** + * @brief Initializes a Conduit `node_m` instance entry for a particle container derived from ParticleBaseBase. + * + * @tparam T Particle container type (must derive from ippl::ParticleBaseBase). + * @param entry The particle container to initialize. + * @param label The label for the container/channel. + */ + template + requires std::derived_from, ParticleBaseBase> + void InitVizChannel( + [[maybe_unused]] + const T& entry + , const std::string label + ); + + + /* SHARED_PTR DISPATCHER - automatically unwraps and dispatches to appropriate overload */ + /** + * @brief Dispatcher for InitVizChannel: unwraps shared_ptr and dispatches to the appropriate overload. + * + * @tparam T Entry type. + * @param entry Shared pointer to the entry. + * @param label The label for the entry/channel. + */ + template + void InitVizChannel( + const std::shared_ptr& entry + , const std::string label + ); + + + + // BASE CASE: + // only enabled if EntryT is NOT derived from ippl::ParticleBaseBase + /** + * @brief Fallback for InitVizChannel: handles types not derived from ParticleBaseBase + * and not having specific overloads. Should never be called since these types are already + * filtered out inside a Visitor struct with AllowedVisType_v. + * + * @tparam T Entry type. + * @param entry The entry to initialize (not a particle container). + * @param label The label for the entry/channel. + */ + template + requires (!std::derived_from, ParticleBaseBase>) + void InitVizChannel( + [[maybe_unused]] + // T&& entry + const T& entry + , const std::string label + ); + + + + // ========================================================== + // VISUALIZATION CHANNEL EXECUTIONERS ======================= + // ========================================================== + + + /* SCALAR FIELDS - handles both reference and shared_ptr */ + /* VECTOR FIELDS - handles both reference and shared_ptr */ + // == ippl::Field, Cell> + // == ippl::Field, 3, ippl::UniformCartesian, Cell> + /** + * @brief Executes a scalar/vector field entry, populating the Conduit `node_m` instance and updating the view registry. + * + * @tparam T Field value type. + * @tparam Dim Field dimension. + * @tparam ViewArgs Additional template arguments for the field. + * @param entry The scalar/vector field to execute. + * @param label The label for the field/channel. + * + */ + template + void ExecVizChannel( + const Field& entry + , const std::string label + ); + + + // PARTICLECONTAINERS DERIVED FROM PARTICLEBASE: + /** + * @brief Executes a particle container entry (derived from ParticleBaseBase), populating the Conduit `node_m` instance and updating the view registry. + * + * @tparam T Particle container type (must derive from ippl::ParticleBaseBase). + * @param entry The particle container to execute. + * @param label The label for the container/channel. + */ + template + requires (std::derived_from, ParticleBaseBase>) + void ExecVizChannel( + const T& entry + , const std::string label + ); + + + // BASE CASE: only enabled if EntryT is NOT derived from ippl::ParticleBaseBase + /** + * @brief Fallback for ExecVizChannel: handles types not derived from ParticleBaseBase. + * + * @tparam T Entry type. + * @param label The label for the entry/channel. + * @param entry The entry to execute (not a particle container). + */ + template + requires (!std::derived_from, ParticleBaseBase>) + void ExecVizChannel( + [[maybe_unused]] T&& entry + , const std::string label + ); + + + /* SHARED_PTR DISPATCHER - automatically unwraps and dispatches to appropriate overload */ + /** + * @brief Dispatcher for ExecVizChannel: unwraps shared_ptr and dispatches to the appropriate overload. + * + * @tparam T Entry type. + * @param entry Shared pointer to the entry. + * @param label The label for the entry/channel. + */ + template + void ExecVizChannel( + const std::shared_ptr& entry + , const std::string label + ); + + + + // ========================================================== + // STEERING CHANNEL INITIALIZERS===== ======================= + // ========================================================== + + /** + * @brief Initializes a steerable channel in the Conduit `node_m` instance for runtime parameter adjustment. + * + * @tparam T Type of the steerable parameter. + * @param steerableScalarForwardpass The initial value to set. + * @param label The label for the steerable channel. + */ + template + requires (!std::is_enum_v>) + void InitSteerChannel( const T& steerableScalarForwardpass, const std::string& label ); + + // Enum overloads (arbitrary enum types) + template + requires (std::is_enum_v>) + void InitSteerChannel( const E& e, const std::string& label ); + + // Bool-like switch overload + void InitSteerChannel( const bool& sw, const std::string& label ); + + // Vector overloads for steerable channels + template + void InitSteerChannel( const ippl::Vector& steerableVecForwardpass, const std::string& label ); + + + void InitSteerChannel( const ippl::Button& btn, const std::string& label ); + + // Generic std::vector elements (arithmetic/bool/Button) for array steerables + template + requires (std::is_arithmetic_v> || std::is_enum_v> || std::is_same_v, bool> || std::is_same_v, ippl::Button>) + void InitSteerChannel( const std::vector& arr, const std::string& label ); + + // std::vector of ippl::Vector steerables + template + void InitSteerChannel( const std::vector>& arr, const std::string& label ); + + + // ========================================================== + // STEERING CHANNEL EXECUTIONERS============================= + // ========================================================== + + /** + * @brief Adds a steerable channel to the Conduit `node_m` instance for runtime parameter adjustment. + * + * @tparam T Type of the steerable parameter. + * @param steerableScalarForwardpass The value to pass forward. + * @param steerableSuffix Suffix for the steerable channel name. + */ + template + requires (!std::is_enum_v>) + void ForwardSteerChannel(const T& steerableScalarForwardpass, const std::string& steerableSuffix); + + // Enum overloads (arbitrary enum types) + template + requires (std::is_enum_v>) + void ForwardSteerChannel(const E& e, const std::string& steerableSuffix); + + // Bool-like switch overload + void ForwardSteerChannel(const bool& sw, const std::string& steerableSuffix); + + // Button-like push overloads + void ForwardSteerChannel(const ippl::Button& btn, const std::string& steerableSuffix); + + // Vector overloads for steerable channels + template + void ForwardSteerChannel(const ippl::Vector& steerableVecForwardpass, const std::string& steerableSuffix); + + // Generic std::vector elements (arithmetic/bool/Button) for array steerables + template + requires (std::is_arithmetic_v> || std::is_enum_v> || std::is_same_v, bool> || std::is_same_v, ippl::Button>) + void ForwardSteerChannel(const std::vector& arr, const std::string& label); + + // std::vector of ippl::Vector arrays forward + template + void ForwardSteerChannel(const std::vector>& arr, const std::string& label); + + + + // ========================================================== + // STEERING CHANNEL FETCHERS ============================= + // ========================================================== + + /** + * @brief Fetches the value of a steerable channel from Catalyst results. + * + * @tparam T Type of the steerable parameter. + * @param steerableScalarBackwardpass Reference to store the fetched value. + * @param steerableSuffix Suffix for the steerable channel name. + */ + template + requires (!std::is_enum_v>) + void FetchSteerChannel( T& steerableScalarBackwardpass, const std::string& steerableSuffix); + + // Enum overloads (arbitrary enum types) + template + requires (std::is_enum_v>) + void FetchSteerChannel( E& e, const std::string& steerableSuffix); + + // ippl Vector overload + template + void FetchSteerChannel( ippl::Vector& steerableVecBackwardpass, const std::string& steerableSuffix); + + // standard vector overload for basic types + template + requires (std::is_arithmetic_v> || std::is_enum_v> || std::is_same_v, bool> || std::is_same_v, ippl::Button>) + void FetchSteerChannel( std::vector& out, const std::string& label); + + // std::vector of ippl::Vector arrays backward + template + void FetchSteerChannel( std::vector>& out, const std::string& label); + + + /** + * @brief Retrieves results from Catalyst and populates the given Conduit `node_m` instance. + * + */ + void fetchResults(); + + + // ===================================================================================== + // CatalystAdaptor Public Methods + // ===================================================================================== + public: + + /** + * @brief Struct steering registration. + * + * Expose any user struct composed of already supported steerable member + * types (arithmetic, bool, ippl::Button, ippl::Vector<>, enums). Nested structs are not + * supported yet. Must be called before adding the struct instance to a + * runtime registry. Args must be an even-sized pack of: name(string-like), pointer-to-member. + * Validates each member type at registration (throws IpplException if invalid). + * Stores three lambdas which expand the pack and delegate to visitor overloads. + * + * @tparam T The struct type to register. + * @tparam Args Types of the member pointers. + * @param args Pointers to the struct members. + */ + template + static void RegisterStructMembers(Args&&... args); + + + + /* DEPRECATED atm ... */ + // Optional enum metadata: label -> list of (text,value) choices + // std::unordered_map>> enumChoices_m; + // /** + // * @brief Provides enum choices metadata so the GUI shows a dropdown. + // * + // * Use this before InitializeRuntime. + // * Example: RegisterEnumChoices("mode", {{"Off",0},{"Basic",1},{"Advanced",2}}); + // * + // * @param label The label for the enum choice. + // * @param entries A vector of pairs containing the display name and integer value. + // */ + // void RegisterEnumChoices(const std::string& label, + // const std::vector>& entries) { + // enumChoices_m[label] = entries; + // } + + // /** + // * @brief Provides typed enum choices metadata mapped to a specific label for the GUI dropdown. + // * + // * @tparam E The enumeration type. + // * @param label The specific label of the steerable channel to associate the choices with. + // * @param entries A vector of pairs containing the display name and the enumeration value. + // */ + // template + // requires (std::is_enum_v>) + // void RegisterEnumChoicesTyped(const std::string& label, const std::vector>& entries); + + /** + * @brief Provides typed enum choices metadata registered by the enum type, to be reused across labels. + * + * @tparam E The enumeration type. + * @param entries A vector of pairs containing the display name and the enumeration value. + */ + template + requires (std::is_enum_v>) + void RegisterEnumChoicesTyped(const std::vector>& entries); + + + + + /** + * @brief Initializes Catalyst using runtime registries (visualization and steering). + * + * @param visReg Shared pointer to the visualization runtime registry. + * @param steerReg Shared pointer to the steering runtime registry. + */ + void Initialize( + const std::shared_ptr& visReg, + const std::shared_ptr& steerReg + ); + + /** + * @brief Explicitly forces a host copy for a specifically labelled channel right now. + * + * @param label The label specifying which field or particle to remember currently. + */ + void rememberNow(const std::string label); + + /** + * @brief Executes Catalyst for a given timestep using the runtime registry. + * + * Populates forward steerable values and fetches back updated ones. + * + * @param cycle The current simulation cycle or timestep index. + * @param time The current simulation time. + * @param rank The MPI rank of the executing process (defaults to ippl::Comm->rank()). + */ + void Execute( + int cycle, double time, + int rank = ippl::Comm->rank() + ); + + + /** + * @brief Finalizes Catalyst and releases resources. + */ + void Finalize(); + + + +};//class CatalystAdaptor +} //namespace ippl + +#include "Stream/InSitu/CatalystVisitors.h" +#include "Stream/Registry/VisRegistryRuntime.h" // visitor structs +#include "CatalystAdaptor.hpp" + + + +#endif + diff --git a/src/Stream/InSitu/CatalystAdaptor.hpp b/src/Stream/InSitu/CatalystAdaptor.hpp new file mode 100644 index 000000000..9c22b3a55 --- /dev/null +++ b/src/Stream/InSitu/CatalystAdaptor.hpp @@ -0,0 +1,1158 @@ +#pragma once +#include "Stream/InSitu/CatalystAdaptor.h" + +#include "Ippl.h" +#include +#include +#include +#include +namespace ippl{ + +// ============================================================================================== +// HELPERS ===================================================================================== +// ============================================================================================== +void CatalystAdaptor::setNodeScript( + conduit_cpp::Node nodePath, + const std::string envVar, + const std::filesystem::path defaultFilePath +){ + const char* filePathEnv = std::getenv(envVar.c_str()); + std::filesystem::path filePath; + if (filePathEnv && std::filesystem::exists(filePathEnv)) { + catalystInfo_m << level4 <<"::Initialize()::setNodeScripts(...):" << endl + << " Using " << envVar << " from environment:" << endl + << " "<< filePathEnv << endl; + filePath = filePathEnv; + } else { + catalystInfo_m << level4 <<"::Initialize()::setNodeScripts(...): No valid " << envVar <<" set." << endl + << " Using default:" << endl + << " " << defaultFilePath << endl; + filePath = defaultFilePath; + } + nodePath.set(filePath.string()); +} + + +// ============================================================================================== +// VIS CHANNEL INITIALIZER:====================================================================== +// ============================================================================================== + +// == ippl::Field, Cell> +template +void CatalystAdaptor::InitVizChannel( [[maybe_unused]] const Field& entry , const std::string label) +{ + catalystInfo_m << level4 <<"::Initialize()::InitVizChannel(ippl::Field<" << typeid(T).name() << "," << Dim << ">) called" << endl; + forceHostCopy_m[label] = false; + + const std::string channelName = "ippl_sField_" + label; + if(pngExtracts_m){ + const std::string script = "catalyst/scripts/" + label; + setNodeScript( node_m[script + "/filename"], + "CATALYST_EXTRACTOR_SCRIPT_" +label, + sourceDir_m /"catalyst_scripts" / "catalyst_extractors" /"png_ext_sfield.py" + ); + conduit_cpp::Node args = node_m[script + "/args"]; + args.append().set_string("--channel_name"); + args.append().set_string(channelName); + args.append().set_string("--label"); + args.append().set_string(label); + if(TestName){ + args.append().set_string("--experiment_name"); + args.append().set_string(std::string(TestName)); + } + + args.append().set_string("--verbosity"); + args.append().set_string(std::to_string(catalystInfo_m.getOutputLevel())); + } + + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(channelName); +} + + +// == ippl::Field, 3, ippl::UniformCartesian, Cell> +template +void CatalystAdaptor::InitVizChannel( [[maybe_unused]] const Field, Dim, ViewArgs...>& entry , const std::string label) +{ + catalystInfo_m << "::Initialize()::InitVizChannel(ippl::Field," << Dim + << ">) called" << endl; + + forceHostCopy_m[label] = false; + + + const std::string channelName = "ippl_vField_" + label; + if(pngExtracts_m){ + const std::string script = "catalyst/scripts/" + label; + + setNodeScript( node_m[script + "/filename"], + "CATALYST_EXTRACTOR_SCRIPT_" + label, + sourceDir_m /"catalyst_scripts" / "catalyst_extractors" /"png_ext_vfield.py" + + ); + conduit_cpp::Node args = node_m[script + "/args"]; + args.append().set_string("--channel_name"); + args.append().set_string(channelName); + args.append().set_string("--label"); + args.append().set_string(label); + if(TestName){ + args.append().set_string("--experiment_name"); + args.append().set_string(std::string(TestName)); + } + args.append().set_string("--verbosity"); + args.append().set_string(std::to_string(catalystInfo_m.getOutputLevel())); + } + + + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(channelName); + +} + +// PARTICLECONTAINERS derived from ParticleBaseBase: +// == ippl::ParticleBase,...>,...> +template +requires (std::derived_from, ParticleBaseBase>) +void CatalystAdaptor::InitVizChannel( [[maybe_unused]] const T& entry, const std::string label) +{ + catalystInfo_m << "::Initialize()::InitVizChannel(ParticleBase).name() << ","<< particle_dim_v + << ",...>...> [or subclass]) called" << endl; + + forceHostCopy_m[label] = false; + + const std::string channelName = "ippl_particles_" + label; + if(pngExtracts_m){ + const std::string script = "catalyst/scripts/"+ label; + + setNodeScript( + node_m[script + "/filename"], + "CATALYST_EXTRACTOR_SCRIPT_" +label, + sourceDir_m /"catalyst_scripts" / "catalyst_extractors" /"png_ext_particle.py" + ); + + conduit_cpp::Node args = node_m[script + "/args"]; + args.append().set_string("--channel_name"); + args.append().set_string(channelName); + args.append().set_string("--label"); + args.append().set_string(label); + + if(TestName){ + args.append().set_string("--experiment_name"); + args.append().set_string(std::string(TestName)); + } + + args.append().set_string("--verbosity"); + args.append().set_string(std::to_string(catalystInfo_m.getOutputLevel())); + } + + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(channelName); +} + +/* SHARED_PTR DISPATCHER - automatically unwraps and dispatches to appropriate overload */ +template +void CatalystAdaptor::InitVizChannel( const std::shared_ptr& entry, const std::string label) +{ + if (entry) { + InitVizChannel( *entry + , label + ); + } + else { + catalystWarn_m << "::Initialize()InitVizChannel(nullptr): nullptr passed as entry." << endl + << " ID: "<< label << endl + << " ==> Channel will not be registered in Conduit Node." << endl; + } +} + + +// BASE CASE: +template +requires (!std::derived_from, ParticleBaseBase>) +void CatalystAdaptor::InitVizChannel([[maybe_unused]] const T& entry, const std::string label) +{ + catalystWarn_m << "::Initialize()InitVizChannel(nullptr): Entry type can't be processed." << endl + << " ID: "<< label << endl + << " Type: "<< typeid(std::decay_t).name() << endl + << " ==>Channel will not be registered in Conduit Node." << endl + << " If you see this something is wrong with: CatalystAdaptor::InitVisitor!!" << endl; +} + + + + +// == ippl::Field, Cell>* +// == ippl::Field, 3, ippl::UniformCartesian, Cell>* +template +void CatalystAdaptor::ExecVizChannel(const Field& entry, const std::string label) +{ + const bool refreshOnly = viewRegistry_m.contains(label); + if (refreshOnly && !forceHostCopy_m[label]) { + return; + } + + using Field_type = Field; + const Field_type* field = &entry; + + std::string channelName; + if constexpr (std::is_scalar_v) { + channelName = "ippl_sField_" + label; + catalystInfo_m << level4 <<"::Execute()::ExecVizChannel(" << label << ") | Type:ippl::Field<" << typeid(T).name() << "," << Dim << ">) called" << endl; + + } else if constexpr (is_vector_v) { + channelName = "ippl_vField_" + label; + catalystInfo_m << level4 <<"::Execute()::ExecVizChannel(" << label << ") | Type: ippl::Field::dim << ">," << Dim << ">)" << endl; + }else{ + channelName = "ippl_errorField_" + label; + + catalystInfo_m << "::Execute()::ExecVizChannel(Field<"<)" << endl + << " For this type of Field the Conduit Blueprint description wasnt \n" + << " implemented in ippl. Therefore this type of field is not \n" + << " supported for visualisation." << endl; + } + + conduit_cpp::Node field_node; + typename Field_type::Layout_t& Layout_ = field->getLayout(); + typename Field_type::Mesh_t& Mesh_ = field->get_mesh(); + + const auto LocalNDIndex_ = Layout_.getLocalNDIndex(); + const auto Origin_ = Mesh_.getOrigin(); + const auto Spacing_ = Mesh_.getMeshSpacing(); + + const size_t nGhost = field->getNghost(); // returns int + + const size_t extra = (useGhostMasks_m) ? size_t(2*nGhost) : 0 ; + const size_t index_offset = (useGhostMasks_m) ? size_t(nGhost) : 0 ; + + int dims_n=1; + double extra_origin=0; + + const auto Ox = Origin_[0] + (double(int(LocalNDIndex_[0].first()) - int(index_offset)) + extra_origin) * Spacing_[0]; + const auto Oy = Origin_[1] + (double(int(LocalNDIndex_[1].first()) - int(index_offset)) + extra_origin) * Spacing_[1]; + const auto Oz = Origin_[2] + (double(int(LocalNDIndex_[2].first()) - int(index_offset)) + extra_origin) * Spacing_[2]; + + const size_t nx = LocalNDIndex_[0].length() + extra ; + const size_t ny = (Dim >= 2) ? LocalNDIndex_[1].length() + extra : 1; + const size_t nz = (Dim >= 3) ? LocalNDIndex_[2].length() + extra : 1; + + const auto& fullDeviceView = field->getView(); // original view + using DeviceView_t = typename Field::view_type; + using HostView_t = Kokkos::View< + typename DeviceView_t::data_type, + Kokkos::LayoutLeft, + Kokkos::HostSpace + >; + + if (refreshOnly) { + HostView_t* hostMirrorFinal = viewRegistry_m.find(label); + if (!hostMirrorFinal) { + throw IpplException("Stream::InSitu::CatalystAdaptor::ExecVizChannel", + "Missing host mirror for refresh: " + label); + } + if (useGhostMasks_m) { + Kokkos::deep_copy(*hostMirrorFinal, fullDeviceView); + } else { + auto r0 = Kokkos::make_pair(nGhost, nGhost + nx); + auto r1 = (Dim >= 2) + ? Kokkos::make_pair(nGhost, nGhost + ny) + : Kokkos::make_pair(size_t(0), fullDeviceView.extent(1)); + auto r2 = (Dim >= 3) + ? Kokkos::make_pair(nGhost, nGhost + nz) + : Kokkos::make_pair(size_t(0), fullDeviceView.extent(2)); + Kokkos::deep_copy(*hostMirrorFinal, Kokkos::subview(fullDeviceView, r0, r1, r2)); + } + return; + } + + // channel for this field of type mesh adheres to conduits mesh blueprint + auto channel = node_m["catalyst/channels/"+ channelName]; + auto channel_state = channel["state"]; + + channel["type"].set_string("mesh"); + auto data = channel["data"]; + + + auto fields = data["fields"]; + field_node = fields[label]; + data["topologies/fmesh_topo/type"].set_string("uniform"); + data["topologies/fmesh_topo/coordset"].set_string("cart_uniform_coords"); + data["coordsets/cart_uniform_coords/type"].set_string("uniform"); + + const void* meshKey = static_cast(&Mesh_); + const void* layoutKey = static_cast(&Layout_); + auto ghostKey = GhostKey_t{meshKey, layoutKey, nGhost}; + + const size_t localNumCells = nx * ny * nz; + const int rank = ippl::Comm->rank(); + + // using RankViewCells_t = Kokkos::View; + // RankViewCells_t rank_id_view_cells("rank_id_view_cells", localNumCells); + // auto host_policy = getRangePolicy(rank_id_view_cells); + + using RankViewCells_t = Kokkos::View; + using HostExecSpace = Kokkos::DefaultHostExecutionSpace; + RankViewCells_t rank_id_view_cells("rank_id_view_cells_3D", nx, ny, nz); + + if (localNumCells > 0) { + Kokkos::MDRangePolicy> host_policy( + {0, 0, 0}, // Start indices {i, j, k} + {nx, ny, nz} // End indices {i, j, k} + ); + Kokkos::parallel_for("fill_rank_ids_3D", host_policy, + KOKKOS_LAMBDA(const int i, const int j, const int k) { + rank_id_view_cells(i, j, k) = rank; + }); + } + + auto rank_field = fields["RankID"]; + rank_field["association"].set_string("element"); // associate_m); + rank_field["topology"].set_string("fmesh_topo"); + rank_field["volume_dependent"].set_string("false"); + if (localNumCells > 0) { + rank_field["values"].set_external(rank_id_view_cells.data(), localNumCells); + } else { + rank_field["values"].set_external(static_cast(nullptr), 0); + } + data["metadata/vtk_fields/RankID/attribute_type"].set_string("ProcessIds"); + viewRegistry_m.set(label + "_rank_id_cells", rank_id_view_cells); + + + // auto print_ranked_mesh_info = [&](){ + // catalystWarn_m << "[ rank=" << ippl::Comm->rank() << "]" + // << " | dims(points)=" << nx << "x" << ny << "x" << nz + // << " | ghost: " << nGhost + // << " | origin=(" << Ox << "," << Oy << "," << Oz << ")" + // << " | spacing=(" << Spacing_[0] << "," << (Dim>=2?Spacing_[1]:0)<< "," << (Dim>=3?Spacing_[2]:0) << ")" << endl; + // }; + + // #if defined(MPI_VERSION) + // MPI_Barrier(MPI_COMM_WORLD); + // if(ippl::Comm->rank()==0) print_ranked_mesh_info(); + // MPI_Barrier(MPI_COMM_WORLD); + // if(ippl::Comm->rank()==1) print_ranked_mesh_info(); + // MPI_Barrier(MPI_COMM_WORLD); + // #endif + + { + data["coordsets/cart_uniform_coords/dims/i"].set(nx+ dims_n ); + data["coordsets/cart_uniform_coords/spacing/dx"].set(Spacing_[0]); + data["coordsets/cart_uniform_coords/origin/x"].set( Ox ); + data["topologies/fmesh_topo/origin/x"].set( Ox ); + } + if constexpr(Dim >= 2){ + data["coordsets/cart_uniform_coords/dims/j"].set(ny+ dims_n); + data["coordsets/cart_uniform_coords/spacing/dy"].set(Spacing_[1]); + data["coordsets/cart_uniform_coords/origin/y"].set( Oy ); + data["topologies/fmesh_topo/origin/y"].set( Oy ); + } + if constexpr(Dim >= 3){ + data["coordsets/cart_uniform_coords/dims/k"].set(nz+ dims_n); + data["coordsets/cart_uniform_coords/spacing/dz"].set(Spacing_[2]); + data["coordsets/cart_uniform_coords/origin/z"].set( Oz ); + data["topologies/fmesh_topo/origin/z"].set( Oz ); + } + + // ================================== + // Prepare Field Data in a HostMirror + // ================================== + + // Version 1: Cut out Ghost Cells from data during a deep copy into a new Kokkos View. + auto getHostMirrorView_noGhosts = [&]() -> HostView_t { + + auto r0 = + Kokkos::make_pair(nGhost, nGhost + nx); + auto r1 = (Dim >= 2) + ? Kokkos::make_pair(nGhost, nGhost + ny) + : Kokkos::make_pair(size_t(0), fullDeviceView.extent(1)); + auto r2 = (Dim >= 3) + ? Kokkos::make_pair(nGhost, nGhost + nz) + : Kokkos::make_pair(size_t(0), fullDeviceView.extent(2)); + + + auto deviceSubView = Kokkos::subview(fullDeviceView, r0, r1, r2); + + HostView_t hostMirrorFinal = HostView_t("hostMirrorNoGhosts_LayoutLeft", nx, ny, nz); + // This single deep_copy now performs: + // - Device-to-Host transfer + // - LayoutRight-to-LayoutLeft transpose + // - Cutting Ghost Cells from data. + Kokkos::deep_copy(hostMirrorFinal, deviceSubView); + return hostMirrorFinal ; + + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // NOTE: + // for both lambdas: + // Technically, we need to use deep copy: 1. when explicitly forced 2. In case of Different memory spaces. + // 3.(?) when layouts don't match up + // Even if we can't use a subview directly since data of subview isn't meaningfull accessible in raw format + // with data() (so the subview can't be used by Conduit), but Conduit has it's own methods to access a + // substructures of arrays. See: Conduit Strided Structured Field descriptions. + // more efficient versions without default deep copies should be possible partially relying on shallow copies. + // + // if (!forceHostCopy_m[label] && std::is_same::value) + // HostView_t hostMirrorFinal = HostView_t("hostMirrorNoGhosts",nx,ny,nz); + // -> use data directy if possible eg + // HostView_t hostMirrorFinal = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), fullDeviceView); + // return hostMirrorFinal; + // + // + // (?) create_mirror_and_copy, adapts to spaces! but can convert from LayoutRight to LayoutLeft? + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + }; + + + // Version 2: Mark Ghost Cells in data with VTK meta data. + auto getHostMirrorView_withGhosts = [&]() -> HostView_t + { + using m_t = unsigned char; // Element Type for Field Mask + + /* Define the N-D Host View types we needed for the upcoming section/copying. */ + using DeviceMaskView_t = typename Field::view_type; //Fetch original Field View Type + using HostMaskView1D_t = Kokkos::View; // Host STORAGE + using HostMaskView_t = Kokkos::View< + typename DeviceMaskView_t::data_type, // e.g., unsigned char*** + Kokkos::LayoutLeft, + Kokkos::HostSpace + >; // Host WRAPPER + HostMaskView1D_t hostMaskView1D; // Declare storage view + + + // --- START OF CACHING LOGIC --- + auto it = ghostMaskCache_m.find(ghostKey); + if (it != ghostMaskCache_m.end()) + { + // --- CACHE HIT ---// Re-use the existing ghost view from the cache + catalystInfo_m << level4 <<"::Execute()::ExecVizChannel(" << label << ") | GhostCache HIT" << endl; + hostMaskView1D = it->second; + } + else + { + // --- CACHE MISS --- + catalystInfo_m << level4 <<"::Execute()::ExecVizChannel(" << label << ") | GhostCache MISS" << endl; + + /* Allocate a mask field matching the source field (same mesh/layout/nghost) */ + Field ghostMaskField(Mesh_, Layout_, static_cast(nGhost)); + + /* Fill entire allocation (owned + ghosts) with 1 */ + ghostMaskField = static_cast(1); + DeviceMaskView_t deviceMaskView = ghostMaskField.getView(); + auto interior = ghostMaskField.template getFieldRangePolicy<>(); + + /* Fill inner cells with 0 */ + // TODO(?): use ippl dimension independent iterators + if constexpr (Dim == 1) { + Kokkos::parallel_for("ZeroOwnedMask1D", interior, KOKKOS_LAMBDA(const int i) { + deviceMaskView(i) = static_cast(0); + }); + } else if constexpr (Dim == 2) { + Kokkos::parallel_for("ZeroOwnedMask2D", interior, KOKKOS_LAMBDA(const int i, const int j) { + deviceMaskView(i,j) = static_cast(0); + }); + } else if ( Dim == 3){ // Dim == 3 + Kokkos::parallel_for("ZeroOwnedMask3D", interior, KOKKOS_LAMBDA(const int i, const int j, const int k) { + deviceMaskView(i,j,k) = static_cast(0); + }); + } else{ + throw IpplException("Stream::InSitu::CatalystAdaptor::Execute()::ExecVizChannel(" + label + ") | Type:ippl::Field<" + typeid(T).name() + "," + std::to_string(Dim) + ">)", + "Unsupported Field Dimnesion (Dim > 3) for Visualisation with Catalyst Paraview"); + } + Kokkos::fence(); + + /* Allocate the 1D host view that will own the memory */ + hostMaskView1D = HostMaskView1D_t("hostGhostMask_1D", deviceMaskView.size()); + + /* Create a temporary, UNMANAGED (8)N-D view that wraps the 1D view's data. + This is our copy target. The constructor handles any rank automatically. + (?) The 8-dim extent constructor is flexible for any dimension up to 8 + but eg 3 analog is not (or it seems so at the moment). + */ + HostMaskView_t hostMaskView_N_Rank( + hostMaskView1D.data(), + deviceMaskView.extent(0), + deviceMaskView.extent(1), + deviceMaskView.extent(2), + deviceMaskView.extent(3), + deviceMaskView.extent(4), + deviceMaskView.extent(5), + deviceMaskView.extent(6), + deviceMaskView.extent(7) + ); + + + // The deep_copy now performs: + // - Device-to-Host transfer + // - LayoutRight-to-LayoutLeft + // - Since ND wraps 1D; Copied data is in the hostMaskView1D owned data + Kokkos::deep_copy(hostMaskView_N_Rank, deviceMaskView); + + // Store the 1D view (which owns the memory) in the cache, enough to keep in memory + ghostMaskCache_m[ghostKey] = hostMaskView1D; + // --- END OF CACHING LOGIC --- + + //////////////////////////////////////////////////////////////////////////////////////// + // NOTE: + // I am unsure why a 3D wrapper would not succeed also, but currently a + // code along the following line will run into compilation issues (?). + // + // using HostMaskView_t = Kokkos::View< + // typename decltype(deviceMaskView)::data_type, + // typename decltype(deviceMaskView)::array_layout, + // Kokkos::HostSpace + // >; + // --- or --- + // using HostMaskView_t = Kokkos::View< + // typename decltype(deviceMaskView)::data_type, + // Kokkos::LayoutLeft, // <-- Use LayoutLeft + // Kokkos::HostSpace + // >; + // + // HostMaskView_t hostMaskView(hostMaskView1D.data(), + // deviceMaskView.extent(0), + // deviceMaskView.extent(1), + // deviceMaskView.extent(2)); + // + // Kokkos::deep_copy(hostMaskView, deviceMaskView); + // + // ghostMaskCache_m[ghostKey] = hostMaskView1D; + //////////////////////////////////////////////////////////////////////////////////////// + } + + + + + // auto ghostMask_field_meta = data["metadata/vtk_fields/GhostMask_field"]; // can't chooses arbitrary name!!!! + auto ghostMask_field_meta = data["metadata/vtk_fields/vtkGhostType"]; + ghostMask_field_meta["attribute_type"] = "Ghosts"; // same as set string??... + // auto ghostMask_field_node = fields["ghostMask_field"]; // can't chooses arbitrary name!!!! must be vtkGhostType + auto ghostMask_field_node = fields["vtkGhostType"]; + ghostMask_field_node["association"].set_string("element"); //associate_m); // vs vertex ... + ghostMask_field_node["topology"].set_string("fmesh_topo"); + ghostMask_field_node["volume_dependent"].set_string("false"); + // ghostMask_field_node["values"].set_external(hostMaskView.data(), hostMaskView.size()); + ghostMask_field_node["values"].set_external(hostMaskView1D.data(), hostMaskView1D.size()); + + + //////////////////////////////////////////////////////////////////////////////////////////// + // Note: + // Field name in the conduit nodes for data/fields and metadata/vtk_fields has to coincide + // so the meta data can be properly associated with the data. + //////////////////////////////////////////////////////////////////////////////////////////// + + HostView_t hostMirrorFinal = HostView_t("hostMirrorWithGhosts_LayoutLeft", nx, ny, nz); + Kokkos::deep_copy(hostMirrorFinal, fullDeviceView); + + return hostMirrorFinal; + // --- END FIX FOR MAIN FIELD --- + }; + + + HostView_t hostMirrorFinal = (useGhostMasks_m) ? getHostMirrorView_withGhosts() : getHostMirrorView_noGhosts(); + /* FOR BOTH CASES FINAL NODE SETTINGS ARE DONE AND WE HAVE THE DATA INSIDE hostMirrorFinal */ + using elem_t = std::remove_pointer_t; + // will return size of vector and amounts of vectors (not size of multiple doubles) + const auto n_elems = hostMirrorFinal.size(); + // Use true element size as stride (handles padding) + static constexpr size_t stride_bytes = sizeof(elem_t); + // offset is zero?? guaranteed? + const size_t offset = 0; + + + field_node["association"].set_string("element"); //associate_m); + field_node["topology"].set_string("fmesh_topo"); + field_node["volume_dependent"].set_string("false"); + if constexpr (std::is_scalar_v) { + // --- SCALAR FIELD CASE --- + field_node["values"].set_external(hostMirrorFinal.data(), n_elems); + } else if constexpr (is_vector_v) { + // --- VECTOR FIELD CASE --- + // stride was 1 in predecessor code? how did this work?... + field_node["values/x"].set_external(&hostMirrorFinal.data()[0][0], n_elems, offset, stride_bytes); + if constexpr (T::dim>=2) field_node["values/y"].set_external(&hostMirrorFinal.data()[0][1], n_elems, offset, stride_bytes); + if constexpr (T::dim>=3) field_node["values/z"].set_external(&hostMirrorFinal.data()[0][2], n_elems, offset, stride_bytes); + } + // else { + // --- INVALID CASE --- + // } + + /* save view so data isn't discarded */ + viewRegistry_m.set(label, hostMirrorFinal); +} + + + +// == PARTICLECONTAINERS derived from ippl::ParticleBase,...>,...> +template +requires (std::derived_from, ParticleBaseBase>) +void CatalystAdaptor::ExecVizChannel(const T& entry, const std::string label) +{ + const bool refreshOnly = viewRegistry_m.contains(label); + if (refreshOnly && !forceHostCopy_m[label]) { + return; + } + + catalystInfo_m << "::Execute()::ExecVizChannel(" << label << ") | Type : ParticleBase).name() + << "," + << particle_dim_v + << ",...>...> [or subclass])" << endl; + const std::string channelName = "ippl_particles_" + label; + + auto particleContainer = &entry; + assert((particleContainer->R.getView().data() != nullptr) && "R view should not be nullptr, might be missing the right execution space"); + + const std::string blockName = "block_allRanks"; + // const std::string blockName = "block_rank" + std::to_string(ippl::Comm->rank()); + + // channel for this particleContainer + // channel of type mesh adheres to conduits mesh blueprint + + + auto channel = node_m["catalyst/channels/"+ channelName]; + + + + channel["type"].set_string("multimesh"); + + + auto data = channel["data/block_main"]; + auto data_help = channel["data/block_help"]; + channel["assembly/main"] = "block_main"; + channel["assembly/help"] = "block_help"; + + //////////////////////////////////////////////////////// + // Note: + // Multimesh currently seems to have caused more headaches compared to what + // we have seemed to have gained from using it. So if the bugs or inconveniences stay + // in upcoming updates for ParaView catalyst we might want to use two normal meshes + // instead. + // channel["type"].set_string("mesh"); + //////////////////////////////////////////////////////// + + auto fields = data["fields"]; + data["type"].set_string("mesh"); + + const size_t localNum = particleContainer->getLocalNum(); + const int rank = ippl::Comm->rank(); + + using IotaView_t = Kokkos::View; + using RankView_t = Kokkos::View; + IotaView_t iota_view("iota", localNum); + RankView_t rank_id_view("rank_id_view", localNum); + if (localNum > 0) { + using HostExecSpace = Kokkos::DefaultHostExecutionSpace; + Kokkos::RangePolicy host_policy(0, localNum); + Kokkos::parallel_for("fill_iota_host", host_policy, KOKKOS_LAMBDA(const int64_t i) { + iota_view(i) = i; + }); + Kokkos::parallel_for("fill_rank_ids", host_policy, KOKKOS_LAMBDA(const int64_t i) { + rank_id_view(i) = rank; + }); + } + + viewRegistry_m.set(label + "_iota", iota_view); + viewRegistry_m.set(label + "_rank_id", rank_id_view); + + // Creates a host-accessible mirror view and copies the data from the device view to the host. + using RAttrib_t = std::remove_reference_tR)>; + using hostMirror_R_t = typename RAttrib_t::host_mirror_type; + hostMirror_R_t R_hostMirror; + + if (forceHostCopy_m[label]) { + R_hostMirror = particleContainer->R.getHostMirror(); + Kokkos::deep_copy(R_hostMirror, particleContainer->R.getView()); + viewRegistry_m.set(R_hostMirror); + } else { + R_hostMirror = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), particleContainer->R.getView()); + viewRegistry_m.set(R_hostMirror); + } + + using hostMirror_ID_t = typename std::remove_reference_tID)>::host_mirror_type; + hostMirror_ID_t ID_hostMirror; + if (forceHostCopy_m[label]) { + ID_hostMirror = particleContainer->ID.getHostMirror(); + Kokkos::deep_copy(ID_hostMirror, particleContainer->ID.getView()); + viewRegistry_m.set(label, ID_hostMirror); + } else { + ID_hostMirror = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), particleContainer->ID.getView()); + viewRegistry_m.set(label, ID_hostMirror); + } + + using PLayout_t = T::Layout_t; + // using vector_t = T::Layout_t::vector_type; + // using value_t = T::Layout_t::value_type; + using R_elem_t = std::remove_pointer_t; //avoids padding etc (?) Rattrib_t + static constexpr size_t R_stride_bytes = sizeof(R_elem_t); + + /* checks if playput it spatial or pure layout */ + if constexpr (has_getRegionLayout_v){ + + using RLayout_t = PLayout_t::RegionLayout_t; + using NDRegion_t = RLayout_t::NDRegion_t; + constexpr unsigned dim_ = PLayout_t::dim; + const NDRegion_t ndr = particleContainer->getLayout().getRegionLayout().getDomain(); + + /* HELPER COORDINATES TO PASS THE BOUNDING BOX in vtk format*/ + /* HELPER TOPOLOGY TO PASS THE BOUNDING BOX (??even needed??) in vtk format */ + data_help["coordsets/bound_helper_coords/type"].set_string("uniform"); + data_help["topologies/bound_helper_topo/coordset"].set_string("bound_helper_coords"); + data_help["topologies/bound_helper_topo/type"].set_string("uniform"); + /* create unfirom coordinate mesh only consisting of the corner points of the domain */ + { + data_help["coordsets/bound_helper_coords/dims/i"].set(2); + data_help["coordsets/bound_helper_coords/spacing/dx"].set( ndr[0].max() - ndr[0].min() ); + data_help["coordsets/bound_helper_coords/origin/x"].set( ndr[0].min() ); + // data_help["topologies/bound_helper_topo/origin/x"].set( ndr[0].min() ); + } + if constexpr(dim_ >= 2){ + data_help["coordsets/bound_helper_coords/dims/j"].set(2); + data_help["coordsets/bound_helper_coords/spacing/dy"].set( ndr[1].max()- ndr[1].min() ); + data_help["coordsets/bound_helper_coords/origin/y"].set( ndr[1].min() ); + // data_help["topologies/bound_helper_topo/origin/y"].set( ndr[1].min() ); + } + if constexpr(dim_ >= 3){ + data_help["coordsets/bound_helper_coords/dims/k"].set(2); + data_help["coordsets/bound_helper_coords/spacing/dz"].set( ndr[2].max()- ndr[1].min() ); + data_help["coordsets/bound_helper_coords/origin/z"].set( ndr[2].min() ); + // data_help["topologies/bound_helper_topo/origin/z"].set( ndr[2].min() ); + } + } + // else { + /* will use raw particle data instead .. */ + // } + + + /* ATTRIBUTES HARDCODED IN PARTICELBASE are identity ID and position R */ + /* EXPLICIT COORDINATES -> EACH PARTICLE'S POSITION */ + + ///////////////////////////////////////////////////////////// + // Note: + // For debuggng purposes checking distribution of particles on ranks... + // #if defined(MPI_VERSION) + // // MPI_Barrier(MPI_COMM_WORLD); + // // if(ippl::Comm->rank()==0) catalystInfo_m << level4 <<"[Rank 0] Local Particles: " << localNum << endl; + // MPI_Barrier(MPI_COMM_WORLD); + // catalystWarn_m << "Local Particles: " << localNum << endl; + // MPI_Barrier(MPI_COMM_WORLD); + // #endif + ///////////////////////////////////////////////////////////// + + data["coordsets/p_explicit_coords/type"].set_string("explicit"); + /* unstructured topology relying on per rank unique particle ID */ + data["topologies/p_unstructured_topo/coordset"].set_string("p_explicit_coords"); + data["topologies/p_unstructured_topo/type"].set_string("unstructured"); + data["topologies/p_unstructured_topo/elements/shape"].set_string("point"); + data["topologies/p_unstructured_topo/elements/connectivity"].set_external(iota_view.data(),particleContainer->getLocalNum()); + + // left hardcodeed we already have the hostViews (instead of integrating the into the loop) + + /* Process ID ATTRIBUTE */ + auto rank_field = fields["RankID"]; + rank_field["association"].set_string("vertex"); + rank_field["topology"].set_string("p_unstructured_topo"); + rank_field["volume_dependent"].set_string("false"); + rank_field["values"].set_external(rank_id_view.data(), localNum); + data["metadata/vtk_fields/RankID/attribute_type"].set_string("ProcessIds"); + + /* Global ID ATTRIBUTE */ + auto id_field = fields["ParticleIDs"]; + id_field["association"].set_string("vertex"); + id_field["topology"].set_string("p_unstructured_topo"); + id_field["volume_dependent"].set_string("false"); + id_field["values"].set_external(ID_hostMirror.data(), localNum); + data["metadata/vtk_fields/ParticleIDs/attribute_type"].set_string("GlobalIds"); + + /* POSITION ATTRIBUTE */ + auto R_field = fields["position"]; + R_field["association"].set_string("vertex"); + R_field["topology"].set_string("p_unstructured_topo"); + R_field["volume_dependent"].set_string("false"); + + + if (localNum > 0) + { + /* COORDINATE DEFINITION... */ + data["coordsets/p_explicit_coords/values/x"].set_external(&R_hostMirror.data()[0][0], particleContainer->getLocalNum(), 0, R_stride_bytes); + data["coordsets/p_explicit_coords/values/y"].set_external(&R_hostMirror.data()[0][1], particleContainer->getLocalNum(), 0, R_stride_bytes); + data["coordsets/p_explicit_coords/values/z"].set_external(&R_hostMirror.data()[0][2], particleContainer->getLocalNum(), 0, R_stride_bytes); + + /* POSITION ATTRIBUTE */ + R_field["values/x"].set_external(&R_hostMirror.data()[0][0], particleContainer->getLocalNum(), 0, R_stride_bytes); + R_field["values/y"].set_external(&R_hostMirror.data()[0][1], particleContainer->getLocalNum(), 0, R_stride_bytes); + R_field["values/z"].set_external(&R_hostMirror.data()[0][2], particleContainer->getLocalNum(), 0, R_stride_bytes); + + /* concept for no copy in situ vis would be */ + //mesh["topologies/p_unstructured_topo/elements/connectivity"].set_external(particleContainer->ID.getView().data(),particleContainer->getLocalNum()); + }else + { + // In case a rank has no particles-> data()[0] is nulllptr dereferencing !!!!! + using component_type = typename R_elem_t::value_type; + data["coordsets/p_explicit_coords/values/x"].set_external(static_cast(nullptr), 0); + data["coordsets/p_explicit_coords/values/y"].set_external(static_cast(nullptr), 0); + data["coordsets/p_explicit_coords/values/z"].set_external(static_cast(nullptr), 0); + R_field["values/x"].set_external(static_cast(nullptr), 0); + R_field["values/y"].set_external(static_cast(nullptr), 0); + R_field["values/z"].set_external(static_cast(nullptr), 0); + } + + // Skip built-in attributes (ID and/or R) and call signConduitBlueprintNode + // on every remaining user-defined attribute. + // + // NOTE: getAttributeNum() counts across ALL memory spaces, while getAttribute(i) + // only indexes into the DefaultExecutionSpace::memory_space vector — a mismatch + // that causes an out-of-bounds segfault when attributes exist in multiple spaces. + // forAllAttributes with void MemorySpace passes each per-space *vector* to the + // functor, so we iterate the vectors ourselves with a global skip counter. + constexpr size_t builtinAttribs = T::EnableIDs ? 2 : 1; + + { + size_t attrib_idx = 0; + entry.forAllAttributes([&](const Attributes& atts) { + for (auto* attribute : atts) { + if (attrib_idx >= builtinAttribs) { + attribute->signConduitBlueprintNode(localNum, fields, viewRegistry_m, catalystInfo_m, catalystWarn_m, forceHostCopy_m[label]); + } + ++attrib_idx; + } + }); + } + //////////////////////////////////////////////////////////////////////////////////////////// + // Note: + // All ways on how to iterate over particle attributes rely on base class pointers. + // In whih case dimensions and types particle attributes is not retrievable from the + // a pointer instance. Therefore conduit maniupulation are done as a membermethod + // for paricleAttrib (overriding a virtual method in the base class). + // + // entry.template forAllAttributes( + // [&](const Attributes& atts) { + // for (auto* attribute : atts) { + //////////////////////////////////////////////////////////////////////////////////////////// + +} + + +// BASE CASE: only enabled if EntryT is NOT derived from ippl::ParticleBaseBase +template +requires (!std::derived_from, ParticleBaseBase>) + void CatalystAdaptor::ExecVizChannel( [[maybe_unused]] T&& entry, const std::string label) +{ + catalystInfo_m << level4 <<" Entry type can't be processed: ID "<< label <<" "<< typeid(std::decay_t).name() << endl; +} + + +/* SHARED_PTR DISPATCHER - automatically unwraps and dispatches to appropriate overload */ +template + void CatalystAdaptor::ExecVizChannel( const std::shared_ptr& entry,const std::string label ) +{ + if (entry) { + catalystInfo_m << level4 <<" dereferencing shared pointer and reattempting execute..." << endl; + ExecVizChannel(*entry, label ); + } else { + catalystInfo_m << level4 <<" Null shared_ptr encountered" << endl; + } +} + + +} + + +// ===================================================================================== +// STEERING: +// ===================================================================================== +#include "Stream/InSitu/CatalystAdaptorSteering.hpp" +// ===================================================================================== + +namespace ippl{ +/////////////////////////////////////////////////////////////// +// Note: +// this does not really need to be a separate function call, +// May we should just inline this into the execute functions ... +/////////////////////////////////////////////////////////////// +void CatalystAdaptor::fetchResults() { + + catalyst_status err = catalyst_results(conduit_cpp::c_node(&results_m)); + if (err != catalyst_status_ok) + { + std::cerr << "Failed to execute Catalyst-results: " << err << std::endl; + } + } + + +// ===================================================================================== +// Runtime registry based Initialize / Execute (non-templated registry) +// ===================================================================================== + +void CatalystAdaptor::Initialize( + const std::shared_ptr& visReg, + const std::shared_ptr& steerReg + ) { +if ( !visEnabled_m) return; + + + catalystInfo_m << level4 <<"::Initialize() START============================================================= 0" << endl; + + int all_ready = 1; + #if defined(MPI_VERSION) + MPI_Allreduce(MPI_IN_PLACE, &all_ready, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); + #endif + catalystInfo_m << level4 <<"::InitializeRuntime() Ranks ready for catalyst int: " << all_ready << " ranks" << endl; + + visRegistry_m = visReg; + steerRegistry_m = steerReg; + + + const int fcomm = MPI_Comm_c2f(MPI_COMM_WORLD); + const int64_t fcomm64 = static_cast(fcomm); + node_m["catalyst/mpi_comm"].set(fcomm64); + + + setNodeScript( node_m["catalyst/scripts/script/filename"], //where in node_m + "CATALYST_PIPELINE_PATH", // environment override + sourceDir_m / "catalyst_scripts" / "pipeline_default.py") //default + ; + conduit_cpp::Node args = node_m["catalyst/scripts/script/args"]; + + + args.append().set_string("--channel_names"); + InitVisitor initV{*this}; + visRegistry_m->forEach(initV); + // Visitor will (also) append channel names here into the node_m (sequence of overall arguments is important!!) + + args.append().set_string("--verbosity"); + args.append().set_string(std::to_string(catalystInfo_m.getOutputLevel())); + + + args.append().set_string("--VTKextract"); + args.append().set_string(std::string(catalystVtk_m)); + + args.append().set_string("--live"); + args.append().set_string(std::string(catalystLive_m)); + + args.append().set_string("--steer"); + args.append().set_string(std::string(catalystSteer_m)); + + + args.append().set_string("--steer_channel_names"); + + auto proxyPath = (sourceDir_m / "catalyst_scripts" / "catalyst_proxy.xml").string() ; + std::string cfgYaml; + if (const char* cfg_env = std::getenv("IPPL_PROXY_CONFIG_YAML")) { + if (std::filesystem::exists(cfg_env)) { + cfgYaml = std::string(cfg_env); + } else { + catalystInfo_m << level4 <<"::Initialize() IPPL_PROXY_CONFIG_YAML set but file not found: '" << cfg_env << "', using default." << endl; + } + } + if (cfgYaml.empty()) { + auto default_cfgYaml = (sourceDir_m / "catalyst_scripts" / "proxy_default_config.yaml").string(); + if (std::filesystem::exists(default_cfgYaml)) { + cfgYaml = std::move(default_cfgYaml); + } // else leave empty -> ProxyWriter can proceed without config + } + + + proxyWriter_m.initialize(proxyPath, cfgYaml); + if (steerEnabled_m ) { + SteerInitVisitor steerInitV{*this}; + steerRegistry_m->forEach(steerInitV); + } + setNodeScript( node_m["catalyst/proxies/proxy_/filename"], + "CATALYST_PROXYS_PATH", + proxyPath + ); + + + + if( std::string(proxyOption_m) == "PRODUCE_ONLY"){ + proxyWriter_m.produceUnified("SteerableParameters_SCALARS", "SteerableParameters"); + throw IpplException("Stream::InSitu::CatalystAdaptor", "write_proxy_only_run: proxies have been printed"); + }else if( std::string(proxyOption_m) == "OFF"){ + }else{ + proxyWriter_m.produceUnified("SteerableParameters_SCALARS", "SteerableParameters"); + } + + + catalystInfo_m << level4 <<"::Initialize() Printing Conduit `node_m` instance passed to catalyst_initialize() =>" << endl; + catalystInfo_m << level4 <second; + forceHostCopy_m[label] = true; + ExecVisitor execV{*this}; + const bool ok = visRegistry_m->forOne(label, execV); + + // Restore prior state + forceHostCopy_m[label] = tmp; + if (!ok) { + throw IpplException("Stream::InSitu::CatalystAdaptor::rememberNow", "Label not found in executable entries or has no execute callback: " + label); + } + +} + +void CatalystAdaptor::Execute( int cycle, double time, int rank /* default = ippl::Comm->rank() */) { + if ( !visEnabled_m) return; + + catalystInfo_m << level4 <<"::Execute() START =============================================================== 0" << endl; + + static IpplTimings::TimerRef TMRcatalyst_execute = IpplTimings::getTimer("catalyst_execute"); + static IpplTimings::TimerRef TMRexecVizVisitor = IpplTimings::getTimer("execVizVisitor"); + static IpplTimings::TimerRef TMRexecSteerVisitor = IpplTimings::getTimer("execSteerVisitor"); + + + auto state = node_m["catalyst/state"]; + state["cycle"].set(cycle); + state["time"].set(time); + state["domain_id"].set(rank); + + IpplTimings::startTimer(TMRexecVizVisitor); + if ( !!visEnabled_m){ + // edit forward Node: add visualisation channels + ExecVisitor execV{*this}; + visRegistry_m->forEach(execV); + } + IpplTimings::stopTimer(TMRexecVizVisitor); + + + IpplTimings::startTimer(TMRexecSteerVisitor); + if (catalystSteer_m && std::string(catalystSteer_m) == "ON") { + // edit forward Node: add steering channels + SteerForwardVisitor steerV{*this}; + steerRegistry_m->forEach(steerV); + } + IpplTimings::stopTimer(TMRexecSteerVisitor); + + + if(cycle == 0){ + + #if defined(MPI_VERSION) + MPI_Barrier(MPI_COMM_WORLD); + catalystInfo_m << level4 <<"::Execute() [rank = 0] Printing first Conduit Node passed from to catalyst_execute() ==>" << endl; + if(catalystInfo_m.getOutputLevel() > 0 && ippl::Comm->rank()==0) node_m.print(); + catalystInfo_m << level4 <<"::Execute() [rank = 1] Printing first Conduit Node passed from to catalyst_execute() ==>" << endl; + MPI_Barrier(MPI_COMM_WORLD); + if(catalystInfo_m.getOutputLevel() > 0 && ippl::Comm->rank()==1) node_m.print(); + MPI_Barrier(MPI_COMM_WORLD); + #endif + // if(level >= 5 && ippl::Comm->rank()==0) node_m.print(); + + catalystInfo_m << "::Execute() During first catalyst_execute() catalyst will " << endl + << " for each passed script - in order how they were " << endl + << " passed to the conduit node - run the globa scope," << endl + << " the initialize() and the execute()." << endl; + } + + + //////////////////////////////////////////////////////////////// + // Note: + // Possibly helpful for further debugging. + // + // Kokkos::fence(); + // #if defined(MPI_VERSION) + // MPI_Barrier(MPI_COMM_WORLD); + // #endif + // + // int all_ready = 1; + // #if defined(MPI_VERSION) + // MPI_Allreduce(MPI_IN_PLACE, &all_ready, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD); + // #endif + // catalystInfo_m << level4 <<"::Execute() All ranks ready for catalyst_execute: " << all_ready << " ranks" << endl; + //////////////////////////////////////////////////////////////// + + catalystInfo_m << level4 <<"::Execute()::catalyst_execute() ==>" << endl; + IpplTimings::startTimer(TMRcatalyst_execute); + catalyst_status err = catalyst_execute(conduit_cpp::c_node(&node_m)); + IpplTimings::stopTimer(TMRcatalyst_execute); + + //////////////////////////////////////////////////////////////////////////////// + // Note: + // catalyst execute seems to be the current bottleneck of a medium sized simulation... + //////////////////////////////////////////////////////////////////////////////// + + + if (err != catalyst_status_ok) { + std::cerr << "::Execute() Failed to execute Catalyst (runtime path): " << err << std::endl; + } + + if (catalystSteer_m && std::string(catalystSteer_m) == "ON") { + + static IpplTimings::TimerRef TMRfetchResult = IpplTimings::getTimer("fetchSteerParameters"); + IpplTimings::startTimer(TMRfetchResult); + + fetchResults(); + // backward Node: fetch updated steering values + SteerFetchVisitor fetchV{*this}; + steerRegistry_m->forEach(fetchV); + + IpplTimings::stopTimer(TMRfetchResult); + + + if(true){ + // if(cycle == 0){ + catalystInfo_m << level4 <<"::Execute() Printing Conduit Node received from catalyst_execute() ==>" << endl; + catalystInfo_m << level4 << results_m.to_yaml() << endl; + } + + } + + + viewRegistry_m.clear(); + ghostMaskCache_m.clear(); + node_m.reset(); + + /////////////////////////////////////////////////// + // Note: + // We deliberately don't reset results since + // 1. they will be properly overwritten by Catalyst. + // 2. If the part of the Catalyst backend crashes and the + // results are not sent back, the old results will be used, possibly + // avoiding a problems during result retrieval. + // + // results.reset(); + /////////////////////////////////////////////////// + + + catalystInfo_m << level4 <<"::Execute() DONE =============================================================== 1" << endl; + +} + + +void CatalystAdaptor::Finalize() { + if ( !visEnabled_m) return; + + conduit_cpp::Node node; + catalyst_status err = catalyst_finalize(conduit_cpp::c_node(&node_m)); + if (err != catalyst_status_ok) { + std::cerr << "Failed to finalize Catalyst: " << err << std::endl; + } +} + + + +}//ippl \ No newline at end of file diff --git a/src/Stream/InSitu/CatalystAdaptorSteering.hpp b/src/Stream/InSitu/CatalystAdaptorSteering.hpp new file mode 100644 index 000000000..0247b6c20 --- /dev/null +++ b/src/Stream/InSitu/CatalystAdaptorSteering.hpp @@ -0,0 +1,934 @@ +#pragma once + +#include "Stream/InSitu/CatalystAdaptor.h" +#include "Stream/InSitu/CatalystVisitors.h" // ensure AllowedSteerType_v available +#include +#include +#include +#include +#include + + + + +namespace ippl{ + +namespace detail { + + ///////////////////////////////////////////////// + // Note: + // Label sanitation: replace '/' to avoid + // unintended Conduit subtree splitting. + // TODO: Shoul be used also in Viz channels + ///////////////////////////////////////////////// + inline std::string sanitize_label(const std::string& in) { + std::string tmp; tmp.reserve(in.size()); + std::string out; out.reserve(in.size()); + for (char c : in) tmp += (c=='/' ? '_' : c); + for (char c : tmp) out += (c=='/' ? '_' : c); + + + return out; + } + + // Recursive applicators with const and non-const overloads to avoid const_cast. + // Base cases (no members left). + template + inline void apply_struct_members(Visitor&, T&, const std::string&) {} + template + inline void apply_struct_members(Visitor&, const T&, const std::string&) {} + + // Recursive steps: visit (name, pointer-to-member) then recurse. + template + inline void apply_struct_members(Visitor& vis, T& obj, const std::string& rootLabel, + NameType name, MemberPtr ptr, Rest&&... rest) { + // Build full label and dispatch to existing visitor overloads. + std::string fullLabel = rootLabel + "." + sanitize_label(std::string(name)); + vis(fullLabel, obj.*ptr); // rely on visitor operator() overload selection + apply_struct_members(vis, obj, rootLabel, std::forward(rest)...); + } + template + inline void apply_struct_members(Visitor& vis, const T& obj, const std::string& rootLabel, + NameType name, MemberPtr ptr, Rest&&... rest) { + std::string fullLabel = rootLabel + "." + sanitize_label(std::string(name)); + vis(fullLabel, obj.*ptr); + apply_struct_members(vis, obj, rootLabel, std::forward(rest)...); + } + + // Meta storage per struct type T. + template + struct StructMeta { + static inline bool registered = false; + // Dispatch lambda handles scalar/member steering. Takes a variant of visitors. + static inline std::function dispatch; + // Array-of-struct aggregation (vector) – built at registration time. + static inline std::function&, const std::string&)> dispatch_vec; + }; + +} // namespace detail + + + +// ===================================================================================== +// Registration functions +// ===================================================================================== +template +void CatalystAdaptor::RegisterStructMembers(Args&&... args) { + using DecayT = std::decay_t; + static_assert(sizeof...(Args) % 2 == 0, "RegisterStructMembers requires (name, memberPtr) pairs"); + if (detail::StructMeta::registered) return; + + // Pack names and member pointers interleaved. + auto pack = std::tuple(std::forward(args)...); + constexpr size_t N = sizeof...(Args); + constexpr size_t PairCount = N / 2; + + // Validate each (name, memberPtr) pair. + [&](std::index_sequence){ + (([] (auto name, auto memberPtr){ + using MemberType = std::decay_t().*memberPtr)>; + if constexpr (!AllowedSteerType_v) { + throw IpplException( + "CatalystAdaptor::RegisterStructMembers", + std::string("Unsupported member type for steering in struct '") + typeid(DecayT).name() + + "' member '" + std::string(name) + "'" + ); + } + })(std::get<2*I>(pack), std::get<2*I+1>(pack)), ...); + }(std::make_index_sequence{}); + + // Visitor dispatch reuses generic applicator. + detail::StructMeta::dispatch = [pack](CatalystAdaptor::SteerVisitorVariant_t var, DecayT& obj, const std::string& root) mutable { + std::visit([&](auto* active_ptr) { + if (!active_ptr) return; + auto& vis = *active_ptr; + std::apply([&](auto&&... all){ ippl::detail::apply_struct_members(vis, obj, ippl::detail::sanitize_label(root), all...); }, pack); + }, var); + }; + + // ================= Array-of-Struct (vector) support ================= + detail::StructMeta::dispatch_vec = [pack](CatalystAdaptor::SteerVisitorVariant_t var, std::vector& arr, const std::string& root) mutable { + if (arr.empty()) return; + constexpr size_t PC = PairCount; + std::visit([&](auto* active_ptr) { + if (!active_ptr) return; + auto& vis = *active_ptr; + using VisitorType = std::decay_t; + + [&](std::index_sequence){ + ( [&](){ + // using MemberPtrT = std::tuple_element_t<2*I+1, decltype(pack)>; + // MemberPtrT = // causese problems with "older" compilers... + auto mptr = std::get<2*I+1>(pack); + auto rawName = std::get<2*I>(pack); + std::string memberLabel = ippl::detail::sanitize_label(root) + '.' + ippl::detail::sanitize_label(std::string(rawName)); + using MType = std::remove_reference_t().*mptr)>; + std::vector tmp; tmp.reserve(arr.size()); + for (auto& el : arr) tmp.push_back(el.*mptr); + + vis(memberLabel, tmp); + + if constexpr (std::is_same_v) { + if (tmp.size() != arr.size()) arr.resize(tmp.size()); + // Write back member values + for (std::size_t i = 0; i < tmp.size(); ++i) { + arr[i].*mptr = tmp[i]; + } + } + }(), ... ); + }(std::make_index_sequence{}); + }, var); + }; + + detail::StructMeta::registered = true; +} + +/* DEPRECATED atm ... */ +// template +// requires (std::is_enum_v>) +// void CatalystAdaptor::RegisterEnumChoicesTyped(const std::string& label, const std::vector>& entries) { +// std::vector> conv; +// conv.reserve(entries.size()); +// for (const auto& p : entries) { +// conv.emplace_back(p.first, static_cast(p.second)); +// } +// RegisterEnumChoices(label, conv); +// } + +template +requires (std::is_enum_v>) +void CatalystAdaptor::RegisterEnumChoicesTyped(const std::vector>& entries) { + std::vector> conv; + conv.reserve(entries.size()); + for (const auto& p : entries) { + conv.emplace_back(p.first, static_cast(p.second)); + } + enumChoicesByType_m[std::type_index(typeid(E))] = std::move(conv); +} + + + + +// ===================================================================================== +// INITIALISATION: +// ===================================================================================== + +template +requires (!std::is_enum_v>) +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const T& steerableScalarForwardpass, const std::string& label ){ + catalystInfo_m << "::Initialize()::InitSteerChannel(" << label << "): | Type: " << typeid(T).name() << endl; + // Only invoke ProxyWriter scalar include for arithmetic types; others are placeholders. + if constexpr (std::is_arithmetic_v>) { + proxyWriter_m.include(steerableScalarForwardpass, label); + } else { + catalystWarn_m << "ProxyWriter placeholder: include() for label '" << label + << "' (type=" << typeid(T).name() << ") not implemented yet (TODO)." << endl; + } + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(label); +} + +// Enum overload (explicit) to ensure proper dropdown setup even if template above is shadowed +template +requires (std::is_enum_v>) +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const E& e, const std::string& label ){ + catalystInfo_m << "::Initialize()::InitSteerChannel(" << label << "): | Type: Enum" << endl; + auto it = enumChoices_m.find(label); + if (it != enumChoices_m.end()) { + proxyWriter_m.includeEnum(label, it->second, static_cast(e)); + } else { + // Try type-based enum choices + auto itt = enumChoicesByType_m.find(std::type_index(typeid(E))); + if (itt != enumChoicesByType_m.end()) { + proxyWriter_m.includeEnum(label, itt->second, static_cast(e)); + } else { + // Fallback to a checkbox if no choices registered + proxyWriter_m.includeBool(label, false); + } + } + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(label); +} + +// Bool-like Switch: init (checkbox in GUI) +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const bool& sw, const std::string& label ){ + catalystInfo_m << "::Initialize()::InitSteerChannel(" << label << "): | Type: Switch" << endl; + proxyWriter_m.includeBool(label, static_cast(sw)); + + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(label); +} + +// Button-like: init (push button in GUI) +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const ippl::Button& btn, const std::string& label ){ + catalystInfo_m << "::Initialize()::InitSteerChannel(" << label << "): | Type: Button" << endl; + proxyWriter_m.includeButton(label); + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(label); +} + +// Vector steerable overloads +template +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const ippl::Vector& steerableVecForwardpass, const std::string& label ) +{ + catalystInfo_m << "::Initialize()::InitSteerChannel(" << label << "): | Vector<" << typeid(T).name() << "," << Dim_v << ">" << endl; + // Register this label as a vector channel in the proxy writer (limit to 3 comps in GUI) + proxyWriter_m.includeVector(label); + (void)steerableVecForwardpass; + + // Ensure the Python pipeline receives this label after `--steer_channel_names` + // so it creates the corresponding forward reader and unified sender wiring. + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(label); +} + + + +// ============================================================= +// Generic std::vector steerables (Array-of-struct members) +// Supports arithmetic, bool and ippl::Button as element types. +// ============================================================= +template +requires (std::is_arithmetic_v> || std::is_enum_v> || std::is_same_v, bool> || std::is_same_v, ippl::Button>) +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const std::vector& arr, const std::string& label ) +{ + // Normalize: ensure 'array:' prefix for any std::vector steerable labels (user shouldn't add it) + const std::string alabel = (label.rfind("array:", 0) == 0) ? label : std::string("array:") + label; + catalystInfo_m << "::Initialize()::InitSteerChannel(" << alabel << "): | Type: std::vector size=" << arr.size() << endl; + // Derive namespace from canonical label of the form "array:." + std::string ns = alabel; + if (ns.rfind("array:", 0) == 0) ns = ns.substr(6); + auto dp = ns.find('.'); + if (dp != std::string::npos) ns = ns.substr(0, dp); + + // Register this array label with ProxyWriter so per-namespace array proxies are generated. + // The incoming label is expected to be of the form "array:." (constructed in RegisterStructMembers). + // Scalar-like element arrays use the generic include() path; ProxyWriter will parse and mark as array. + if constexpr (std::is_arithmetic_v>) { + // Use the first element as a default if available; otherwise 0. + Elem def{}; + if (!arr.empty()) def = arr.front(); + proxyWriter_m.include(def, alabel); + } else if constexpr (std::is_enum_v>) { + // Enum arrays: register as enum with choices; default from first element if present + int def = !arr.empty() ? static_cast(arr.front()) : 0; + // Prefer label-scoped choices, then type-scoped, else fallback to a checkbox + auto it = enumChoices_m.find(alabel); + if (it != enumChoices_m.end()) { + proxyWriter_m.includeEnum(alabel, it->second, def); + } else { + auto itt = enumChoicesByType_m.find(std::type_index(typeid(Elem))); + if (itt != enumChoicesByType_m.end()) { + proxyWriter_m.includeEnum(alabel, itt->second, def); + } else { + // No choices registered; use a checkbox as a minimal fallback UI + proxyWriter_m.includeBool(alabel, false); + } + } + } else if constexpr (std::is_same_v, bool>) { + bool def = !arr.empty() ? static_cast(arr.front()) : false; + proxyWriter_m.includeBool(alabel, def); + } else if constexpr (std::is_same_v, ippl::Button>) { + proxyWriter_m.includeButton(alabel); + } + + // Inform proxy writer about desired initial size for this array namespace + proxyWriter_m.setArrayInitialSize(ns, arr.size()); + + // Inform Python pipeline about each label (still needed for backward mapping) + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(alabel); + +} + +// Init: std::vector> steerables +template +void CatalystAdaptor::InitSteerChannel( [[maybe_unused]] const std::vector>& arr, const std::string& label ) +{ + // Normalize: ensure 'array:' prefix for any std::vector steerable labels + const std::string alabel = (label.rfind("array:", 0) == 0) ? label : std::string("array:") + label; + catalystInfo_m << "::Initialize()::InitSteerChannel(" << alabel << "): | Type: std::vector> size=" << arr.size() << endl; + // Derive namespace from canonical label of the form "array:." + std::string ns = alabel; + if (ns.rfind("array:", 0) == 0) ns = ns.substr(6); + auto dp = ns.find('.'); + if (dp != std::string::npos) ns = ns.substr(0, dp); + + // Register the vector array label with ProxyWriter (parsed as array namespace) + proxyWriter_m.includeVector(alabel); + + // Inform proxy writer about desired initial size for this array namespace + proxyWriter_m.setArrayInitialSize(ns, arr.size()); + + conduit_cpp::Node scriptArgs = node_m["catalyst/scripts/script/args"]; + scriptArgs.append().set_string(alabel); + +} + + + +// ===================================================================================== +// SETTING UP / FORWARDING CONDUIT NODE: +// ===================================================================================== + +// basic integer and double scalar overload +template +requires (!std::is_enum_v>) +void CatalystAdaptor::ForwardSteerChannel( const T& steerableScalarForwardpass, const std::string& steerableSuffix ) +{ + catalystInfo_m << "::Execute()::ForwardSteerChannel(" << steerableSuffix << "); | Type: " << typeid(T).name() << endl; + + auto steerableChannel = node_m["catalyst/channels/steerable_channel_0D_mesh"]; + + steerableChannel["type"].set("mesh"); + auto steerableData = steerableChannel["data"]; + steerableData["coordsets/coords/type"].set_string("explicit"); + steerableData["coordsets/coords/values/x"].set( 0 ); + + steerableData["topologies/sMesh_topo/type"].set("unstructured"); + steerableData["topologies/sMesh_topo/coordset"].set("coords"); + steerableData["topologies/sMesh_topo/elements/shape"].set("point"); + steerableData["topologies/sMesh_topo/elements/connectivity"].set( 0 ); + + + conduit_cpp::Node steerableField = steerableData["fields/steerable_field_f_" + steerableSuffix]; + steerableField["association"].set("vertex"); + steerableField["topology"].set("sMesh_topo"); + steerableField["volume_dependent"].set("false"); + + conduit_cpp::Node values = steerableField["values"]; + + // if constexpr(std::is_enum_v>){ + // values.set(static_cast(steerableScalarForwardpass)); + // } + // else + if constexpr(std::is_scalar_v){ + values.set(steerableScalarForwardpass); + } + else { + throw IpplException("Stream::InSitu::CatalystAdaptor::ForwardSteerChannel", "Unsupported steerable type for channel: " + steerableSuffix); + } +} + +// Enum overload (explicitfor clarity) +template +requires (std::is_enum_v>) +void CatalystAdaptor::ForwardSteerChannel( const E& e, const std::string& steerableSuffix ) +{ + catalystInfo_m << "::Execute()::ForwardSteerChannel(" << steerableSuffix << "); | Type: Enum" << endl; + auto steerableChannel = node_m["catalyst/channels/steerable_channel_0D_mesh"]; + steerableChannel["type"].set("mesh"); + auto steerableData = steerableChannel["data"]; + steerableData["coordsets/coords/type"].set_string("explicit"); + steerableData["coordsets/coords/values/x"].set( 0 ); + steerableData["topologies/sMesh_topo/type"].set("unstructured"); + steerableData["topologies/sMesh_topo/coordset"].set("coords"); + steerableData["topologies/sMesh_topo/elements/shape"].set("point"); + steerableData["topologies/sMesh_topo/elements/connectivity"].set( 0 ); + auto steerableField = steerableData["fields/steerable_field_f_" + steerableSuffix]; + steerableField["association"].set("vertex"); + steerableField["topology"].set("sMesh_topo"); + steerableField["volume_dependent"].set("false"); + steerableField["values"].set(static_cast(e)); +} + +// Bool-like Switch overload: forward as single scalar (0/1) +void CatalystAdaptor::ForwardSteerChannel( const bool& sw, const std::string& steerableSuffix ) +{ + catalystInfo_m << "::Execute()::ForwardSteerChannel(" << steerableSuffix << "); | Type: bool/Switch" << endl; + + auto steerableChannel = node_m["catalyst/channels/steerable_channel_0D_mesh"]; + steerableChannel["type"].set("mesh"); + auto steerableData = steerableChannel["data"]; + steerableData["coordsets/coords/type"].set_string("explicit"); + steerableData["coordsets/coords/values/x"].set( 0 ); + + steerableData["topologies/sMesh_topo/type"].set("unstructured"); + steerableData["topologies/sMesh_topo/coordset"].set("coords"); + steerableData["topologies/sMesh_topo/elements/shape"].set("point"); + steerableData["topologies/sMesh_topo/elements/connectivity"].set( 0 ); + + conduit_cpp::Node steerableField = steerableData["fields/steerable_field_f_" + steerableSuffix]; + steerableField["association"].set("vertex"); + steerableField["topology"].set("sMesh_topo"); + steerableField["volume_dependent"].set("false"); + + int boolAsInt = sw ? 1 : 0; + steerableField["values"].set(boolAsInt); +} + +// Bool-like Button overload: forward as single scalar (0/1) +void CatalystAdaptor::ForwardSteerChannel( const ippl::Button& btn, const std::string& steerableSuffix ) +{ + catalystInfo_m << "::Execute()::ForwardSteerChannel(" << steerableSuffix << "); | Type: Button" << endl; + auto steerableChannel = node_m["catalyst/channels/steerable_channel_0D_mesh"]; + steerableChannel["type"].set("mesh"); + auto steerableData = steerableChannel["data"]; + steerableData["coordsets/coords/type"].set_string("explicit"); + steerableData["coordsets/coords/values/x"].set( 0 ); + + steerableData["topologies/sMesh_topo/type"].set("unstructured"); + steerableData["topologies/sMesh_topo/coordset"].set("coords"); + steerableData["topologies/sMesh_topo/elements/shape"].set("point"); + steerableData["topologies/sMesh_topo/elements/connectivity"].set( 0 ); + + conduit_cpp::Node steerableField = steerableData["fields/steerable_field_f_" + steerableSuffix]; + steerableField["association"].set("vertex"); + steerableField["topology"].set("sMesh_topo"); + steerableField["volume_dependent"].set("false"); + steerableField["values"].set(static_cast(btn ? 1 : 0)); +} + +// ippl::Vector overload: forward single scalar conduit field with 3 components in "values" +////////////////////////////////////////////////// +// Note: +// I think x/y/z and 0/1/2 are valid syntax to send +// multicomponents forward, but in when returned in +// results the usual syntax is 0/1/2. +////////////////////////////////////////////////// +template +void CatalystAdaptor::ForwardSteerChannel( const ippl::Vector& steerableVecForwardpass, const std::string& steerableSuffix ) +{ + catalystInfo_m << "::Execute()::ForwardSteerChannel(" << steerableSuffix << "); | Vector<" << typeid(T).name() << "," << Dim_v << ">" << endl; + + auto steerableChannel = node_m["catalyst/channels/steerable_channel_0D_mesh"]; + steerableChannel["type"].set("mesh"); + auto steerableData = steerableChannel["data"]; + steerableData["coordsets/coords/type"].set_string("explicit"); + steerableData["coordsets/coords/values/x"].set(0); + + steerableData["topologies/sMesh_topo/type"].set("unstructured"); + steerableData["topologies/sMesh_topo/coordset"].set("coords"); + steerableData["topologies/sMesh_topo/elements/shape"].set("point"); + steerableData["topologies/sMesh_topo/elements/connectivity"].set(0); + + const std::string base = std::string("fields/steerable_field_f_") + steerableSuffix; + auto fnode = steerableData[base]; + fnode["association"].set_string("vertex"); + fnode["topology"].set_string("sMesh_topo"); + fnode["volume_dependent"].set_string("false"); + + if constexpr (std::is_integral_v) { + const int vx = static_cast(steerableVecForwardpass[0]); + fnode["values/x"].set(vx); + if constexpr (Dim_v >= 2) { + const int vy = static_cast(steerableVecForwardpass[1]); + fnode["values/y"].set(vy); + } + if constexpr (Dim_v >= 3) { + const int vz = static_cast(steerableVecForwardpass[2]); + fnode["values/z"].set(vz); + } + } else { + const double vx = static_cast(steerableVecForwardpass[0]); + fnode["values/x"].set(vx); + if constexpr (Dim_v >= 2) { + const double vy = static_cast(steerableVecForwardpass[1]); + fnode["values/y"].set(vy); + } + if constexpr (Dim_v >= 3) { + const double vz = static_cast(steerableVecForwardpass[2]); + fnode["values/z"].set(vz); + } + } +} + + +// std::vector overload: forward, publish as 1D mesh array under fields/steerableField_f_