Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
doc/html
doc/latex

experiments/

.aider*
.vscode

Expand Down
681 changes: 681 additions & 0 deletions backends/dealii/include/laplace.h

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions backends/dealii/include/register_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "coral.h"
#include "coral_network.h"
#include "laplace.h"
#include "poisson.h"

/** \cond INTERNAL */
Expand Down Expand Up @@ -164,6 +165,13 @@ namespace coral
&PoissonSolver<dim, spacedim>::solve,
{"PoissonSolver::solve<" + Utilities::dim_string(dim, spacedim) + ">",
"poisson_solver"});

NodeObject::register_type<LaplaceProblem<dim>, bool>("mpi_initialize");
NodeObject::register_method<LaplaceProblem<dim>, void, const std::string &>(
&LaplaceProblem<dim>::run,
{"LaplaceProblem::run<" + Utilities::dim_string(dim, spacedim) + ">",
"laplace_problem",
"output_dir"});
}


Expand Down
70 changes: 69 additions & 1 deletion backends/dealii/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
find_package(GTest)
find_package(MPI)
if(NOT GTest_FOUND)
message(WARNING "GTest not found; skipping deal.II backend tests")
return()
Expand All @@ -11,7 +12,10 @@ include(GoogleTest)
# Keep plugin smoke test in its own executable so it doesn't get "help" from
# backend tests that register types directly.
set(_plugin_test_file ${CMAKE_CURRENT_LIST_DIR}/plugin_smoke_test.cc)
list(REMOVE_ITEM _test_files ${_plugin_test_file})
set(_mpi_test_file ${CMAKE_CURRENT_LIST_DIR}/dealii_mpi.cc)
list(REMOVE_ITEM _test_files ${_plugin_test_file} ${_mpi_test_file})

# BACKEND TESTS

add_executable(dealii_backend_tests ${_test_files})

Expand Down Expand Up @@ -47,6 +51,70 @@ gtest_discover_tests(dealii_backend_tests
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
PROPERTIES LABELS "dealii;backend")

# MPI TESTS

add_executable(dealii_mpi_tests ${_mpi_test_file})

target_include_directories(dealii_mpi_tests
PRIVATE
${CMAKE_CURRENT_LIST_DIR}/../include)

target_link_libraries(dealii_mpi_tests
coral_core
${GTEST_LIBRARIES}
${GTEST_MAIN_LIBRARIES})

if(GTEST_INCLUDE_DIRS)
target_include_directories(dealii_mpi_tests SYSTEM PRIVATE ${GTEST_INCLUDE_DIRS})
endif()

deal_ii_setup_target(dealii_mpi_tests)

if(MSVC)
target_compile_options(dealii_mpi_tests PRIVATE /W0)
else()
target_compile_options(dealii_mpi_tests PRIVATE -w)
endif()

# Prefer running the gtest binary directly (faster iteration than ctest).
add_custom_target(run_mpi_backend_tests
COMMAND ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 "$<TARGET_FILE:dealii_mpi_tests>"
DEPENDS dealii_mpi_tests
USES_TERMINAL)

# Register MPI tests with CTest.
# gtest_discover_tests(EXECUTOR) requires CMake >= 3.29. For 3.28 we set the
# CROSSCOMPILING_EMULATOR target property, which gtest_discover_tests respects.
# A wrapper script is used so discovery (--gtest_list_tests) runs with -n 1
# (single clean output) while actual test execution uses -n 4.
if(MPI_FOUND)
set(_mpi_wrapper ${CMAKE_CURRENT_BINARY_DIR}/mpi_gtest_wrapper.sh)
file(WRITE ${_mpi_wrapper}
"#!/bin/bash
if [[ \"$*\" == *\"--gtest_list_tests\"* ]]; then
exec \"$@\"
else
exec ${MPIEXEC_EXECUTABLE} ${MPIEXEC_NUMPROC_FLAG} 4 \"$@\"
fi
")
file(CHMOD ${_mpi_wrapper}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)

set_target_properties(dealii_mpi_tests PROPERTIES
CROSSCOMPILING_EMULATOR ${_mpi_wrapper})

gtest_discover_tests(dealii_mpi_tests
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
PROPERTIES
LABELS "dealii;mpi"
SKIP_REGULAR_EXPRESSION "\\[ SKIPPED")
else()
message(WARNING "MPI not found; skipping MPI tests registration with ctest")
endif()

# PLUGIN TESTS

add_executable(dealii_plugin_tests ${_plugin_test_file})
target_link_libraries(dealii_plugin_tests coral_core dl ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES})
if(GTEST_INCLUDE_DIRS)
Expand Down
162 changes: 162 additions & 0 deletions backends/dealii/tests/dealii_mpi.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#include <gtest/gtest.h>

#include <filesystem>
#include <memory>

#include "coral.h"
#include "coral_network.h"
#include "register_types.h"
#include "test_utils.h"

using namespace dealii;
using namespace coral;
using coral_test::ScopedTestOutputDir;

/**
* MPI-aware wrapper around ScopedTestOutputDir.
*
* Only the master rank (rank 0) constructs and destructs the underlying
* ScopedTestOutputDir, so all filesystem operations (create/delete) happen
* exactly once. MPI barriers ensure:
* - the directory exists before non-master ranks proceed after construction,
* - all ranks have finished writing before master rank deletes on destruction.
*/
class MPIScopedTestOutputDir
{
public:
MPIScopedTestOutputDir(
const std::string &test_name,
const std::filesystem::path &base_dir = "./test_output")
: path_(base_dir / test_name)
{
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0)
scoped_dir_ = std::make_unique<ScopedTestOutputDir>(test_name, base_dir);
MPI_Barrier(MPI_COMM_WORLD); // ensure dir exists before all ranks proceed
}

~MPIScopedTestOutputDir()
{
MPI_Barrier(MPI_COMM_WORLD); // wait for all ranks to finish writing
// scoped_dir_ destructs only on rank 0; other ranks hold nullptr
}

const std::filesystem::path &
path() const
{
return path_;
}

operator const std::filesystem::path &() const
{
return path_;
}

private:
std::filesystem::path path_;
std::unique_ptr<ScopedTestOutputDir> scoped_dir_;
};

class DealiiMPITest : public ::testing::Test
{
private:
using MPI_Session = Utilities::MPI::MPI_InitFinalize;

inline static std::unique_ptr<MPI_Session> m_mpi_session = nullptr;

protected:
void static SetUpTestSuite()
{
int argc = 0;
char **argv = nullptr;
m_mpi_session = std::make_unique<MPI_Session>(argc, argv, 1);

if (Utilities::MPI::n_mpi_processes(MPI_COMM_WORLD) == 1)
GTEST_SKIP() << "Please run MPI test with non trivial world size.";
}

void static TearDownTestSuite()
{
m_mpi_session.reset();
}
};

TEST_F(DealiiMPITest, Setup)
{
int size;
ASSERT_EQ(MPI_Comm_size(MPI_COMM_WORLD, &size), MPI_SUCCESS);
ASSERT_GE(size, 0);

int rank;
ASSERT_EQ(MPI_Comm_rank(MPI_COMM_WORLD, &rank), MPI_SUCCESS);
ASSERT_GE(rank, 0);
}

TEST_F(DealiiMPITest, LaplaceProblem)
{
MARK_LONG_TEST();

MPIScopedTestOutputDir output_dir("DealiiMPITest_PoissonSolver");

register_non_dimensional_types();
register_dimensional_types<2, 2>();

auto laplace = make_node<LaplaceProblem<2>>();
auto mpi_init = make_node(false);
laplace->set_arguments({mpi_init});
ASSERT_TRUE((*laplace)());
ASSERT_TRUE(laplace->ready());

auto run_method = make_node("LaplaceProblem::run<2>");
auto output_dir_string = make_node(std::string(output_dir.path()));
run_method->set_arguments({laplace, output_dir_string});
(*run_method)();
}

TEST_F(DealiiMPITest, LaplaceProblemNetwork)
{
MARK_LONG_TEST();

MPIScopedTestOutputDir output_dir("dealiiMPITest_LaplaceTransformNetwork");

register_non_dimensional_types();
register_dimensional_types<2, 2>();

Network network;
network.set_touch_file_base_path(output_dir);
network.clear_network();

std::ifstream file(SOURCE_DIR "/test_files/laplace.json");
ASSERT_TRUE(file.is_open()) << "Failed to open laplace.json file.";

nlohmann::json json_data;
file >> json_data;
file.close();

ASSERT_FALSE(json_data.empty()) << "JSON data is empty.";

// Update the initialize constant
// Node 5 contains the bool value
if (json_data["workflow"]["nodes"].contains("5") &&
json_data["workflow"]["nodes"]["5"].contains("value"))
{
json_data["workflow"]["nodes"]["5"]["value"] = "false";
}

// Update the output file path to use the test output directory
// Node 2 contains the output filename
if (json_data["workflow"]["nodes"].contains("2") &&
json_data["workflow"]["nodes"]["2"].contains("value"))
{
json_data["workflow"]["nodes"]["2"]["value"] = output_dir.path().string();
}

network.from_json(json_data);

slog_debug("Network has %u nodes and %u connections",
network.n_nodes(),
network.n_connections());

network.run();
}
13 changes: 13 additions & 0 deletions backends/dealii/tests/test_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,17 @@ namespace coral_test

} // namespace coral_test


#define MARK_LONG_TEST() \
do \
{ \
if (std::getenv("CORAL_KEEP_LONG_TESTS") == nullptr) \
{ \
std::cout << "Skip On" << std::endl; \
GTEST_SKIP() \
<< "Long test and CORAL_KEEP_LONG_TEST disable. Skipping."; \
} \
} \
while (0)

#endif // GTESTS_TEST_UTILS_H
36 changes: 36 additions & 0 deletions test_files/laplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"workflow": {
"nodes": {
"0": {
"type": "LaplaceProblem::run<2>",
"position": { "x": 293.7562932408905, "y": 327.37380674085716 },
"qualified_id": "0"
},
"1": {
"type": "LaplaceProblem<2>",
"position": { "x": -183.0478681433714, "y": 226.5708997085626 },
"qualified_id": "1"
},
"2": {
"type": "std::string",
"position": { "x": -164.89097078018403, "y": 417.02495316403406 },
"value": "/app/build/output/",
"qualified_id": "2"
},
"5": {
"type": "bool",
"position": { "x": -515.9703517123914, "y": 221.60561502104412 },
"value": "true",
"qualified_id": "5"
}
},
"edges": {
"0": { "source": 1, "target": 0, "source_output": 0, "target_input": 0 },
"1": { "source": 2, "target": 0, "source_output": 0, "target_input": 1 },
"2": { "source": 5, "target": 1, "source_output": 0, "target_input": 0 }
}
},
"version": 1,
"author": "dealiix-platform",
"date_time_utc": "2026-03-11T11:35:34.762Z"
}
Loading