From 984f321f6314c86665ad0ddb297e4dbe57e618a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:53:05 +0000 Subject: [PATCH 1/6] Initial plan From 0695690484cf54c794f111ab8216402e340422b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 15:03:17 +0000 Subject: [PATCH 2/6] fix: prevent crash when no root exists in shape nesting graph Agent-Logs-Url: https://github.com/OpenSEMBA/tulip/sessions/0c5dd8f8-dcf6-47c2-9133-1b85d07aec55 Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- src/adapter/ShapesClassification.cpp | 9 +++++++++ test/adapter/AdapterTest.cpp | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/adapter/ShapesClassification.cpp b/src/adapter/ShapesClassification.cpp index 42d7464..dd11ed6 100644 --- a/src/adapter/ShapesClassification.cpp +++ b/src/adapter/ShapesClassification.cpp @@ -334,6 +334,7 @@ bool ShapesClassification::isOpenProblem() const auto roots = nestedGraph.roots(); if (open.size() == 1) return true; + if (roots.empty()) return true; if (roots.size() > 1) return true; if (!roots.empty()) { const auto& root = roots[0]; @@ -415,7 +416,15 @@ EntityMap ShapesClassification::buildVacuumDomain() { EntityMap ShapesClassification::buildClosedVacuumDomain() { const auto roots = nestedGraph.roots(); + if (roots.empty()) { + throw std::runtime_error( + "Unable to build closed vacuum domain: no root entity found."); + } const auto& root = roots[0]; + if (!conductors.count(root)) { + throw std::runtime_error( + "Unable to build closed vacuum domain: root entity is not a conductor."); + } EntityList dom = conductors.at(root); EntityList toRemove; diff --git a/test/adapter/AdapterTest.cpp b/test/adapter/AdapterTest.cpp index 9b5f00a..79f499f 100644 --- a/test/adapter/AdapterTest.cpp +++ b/test/adapter/AdapterTest.cpp @@ -395,3 +395,20 @@ TEST_F(AdapterTest, overlapping_dielectrics_prioritize_higher_relative_permittiv EXPECT_NEAR(rightHighLeftMass, 4.0, 1e-9); EXPECT_NEAR(rightHighRightMass, 8.0, 1e-9); } + +TEST_F(AdapterTest, shapes_classification_without_roots_is_treated_as_open_problem) +{ + gmsh::clear(); + gmsh::model::add("no_roots_case"); + + const EntityList shapes = {}; + const nlohmann::json inputJson = { + {"materials", nlohmann::json::array()}, + {"layers", nlohmann::json::array()} + }; + + ShapesClassification classification(shapes, inputJson); + + EXPECT_TRUE(classification.isOpenCase); + EXPECT_TRUE(classification.isOpenProblem()); +} From 48009f8e677ae179df62cf1dd17885a42f5718e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 15:06:16 +0000 Subject: [PATCH 3/6] test: cover rootless shape classification as open problem Agent-Logs-Url: https://github.com/OpenSEMBA/tulip/sessions/0c5dd8f8-dcf6-47c2-9133-1b85d07aec55 Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- test/adapter/AdapterTest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/adapter/AdapterTest.cpp b/test/adapter/AdapterTest.cpp index 79f499f..c3b8b84 100644 --- a/test/adapter/AdapterTest.cpp +++ b/test/adapter/AdapterTest.cpp @@ -411,4 +411,5 @@ TEST_F(AdapterTest, shapes_classification_without_roots_is_treated_as_open_probl EXPECT_TRUE(classification.isOpenCase); EXPECT_TRUE(classification.isOpenProblem()); + EXPECT_NO_THROW(classification.buildVacuumDomain()); } From 439593eeb4b6e43533a8d5ad9698187a28521f9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 15:16:55 +0000 Subject: [PATCH 4/6] fix: validate required materials/layers sections in input JSON Agent-Logs-Url: https://github.com/OpenSEMBA/tulip/sessions/43d75020-aa34-4e7b-b8c1-a6009d96501a Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- src/adapter/Adapter.cpp | 33 +++++++++++++++++++++++++++++++++ test/adapter/AdapterTest.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/adapter/Adapter.cpp b/src/adapter/Adapter.cpp index abf1571..b57108c 100644 --- a/src/adapter/Adapter.cpp +++ b/src/adapter/Adapter.cpp @@ -566,6 +566,38 @@ void validateLayerMaterialIds(const nlohmann::json& inputJson) } } +void validateRequiredInputSections(const nlohmann::json& inputJson) +{ + const bool hasMaterials = + inputJson.contains("materials") && inputJson["materials"].is_array(); + const bool hasLayers = inputJson.contains("layers") && inputJson["layers"].is_array(); + + if (hasMaterials && hasLayers) { + return; + } + + std::string missingSections; + if (!hasMaterials) { + missingSections += "'materials'"; + } + if (!hasLayers) { + if (!missingSections.empty()) { + missingSections += " and "; + } + missingSections += "'layers'"; + } + + std::string message = + "Invalid input JSON: missing required top-level array section(s): " + + missingSections + "."; + if (inputJson.contains("model")) { + message += " Found 'model'; expected top-level 'materials' and 'layers'."; + } else { + message += " Expected top-level 'materials' and 'layers'."; + } + throw std::runtime_error(message); +} + std::vector buildAcceptedStepNamesForLayer( const nlohmann::json& layer, const std::map& materialTypeById) @@ -799,6 +831,7 @@ void Adapter::initialize(const nlohmann::json& inputJson, caseName_ = caseName; inputDir_ = inputDir; + validateRequiredInputSections(inputJson); validateLayerMaterialIds(inputJson); adapterOptions_ = parseAdapterOptions(inputJson, std::filesystem::path(inputDir_), caseName_); diff --git a/test/adapter/AdapterTest.cpp b/test/adapter/AdapterTest.cpp index c3b8b84..109b7ed 100644 --- a/test/adapter/AdapterTest.cpp +++ b/test/adapter/AdapterTest.cpp @@ -222,6 +222,39 @@ TEST_F(AdapterTest, dielectric_unshielded_pair_fails_if_step_layer_is_not_presen std::runtime_error); } +TEST_F(AdapterTest, two_wires_open_fails_if_layers_section_is_missing) +{ + const std::string caseName = "two_wires_open"; + nlohmann::json inputJson = readInputJsonFromCaseName(caseName); + inputJson.erase("layers"); + inputJson["model"] = {{"materials", nlohmann::json::array()}}; + + try { + Adapter adapter(inputJson, caseName, inputFolderFromCaseName(caseName)); + FAIL() << "Expected runtime_error"; + } catch (const std::runtime_error& err) { + const std::string message = err.what(); + EXPECT_NE(message.find("'layers'"), std::string::npos); + EXPECT_NE(message.find("top-level 'materials' and 'layers'"), std::string::npos); + } +} + +TEST_F(AdapterTest, two_wires_open_fails_if_materials_section_is_missing) +{ + const std::string caseName = "two_wires_open"; + nlohmann::json inputJson = readInputJsonFromCaseName(caseName); + inputJson.erase("materials"); + + try { + Adapter adapter(inputJson, caseName, inputFolderFromCaseName(caseName)); + FAIL() << "Expected runtime_error"; + } catch (const std::runtime_error& err) { + const std::string message = err.what(); + EXPECT_NE(message.find("'materials'"), std::string::npos); + EXPECT_NE(message.find("top-level 'materials' and 'layers'"), std::string::npos); + } +} + TEST_F(AdapterTest, dielectric_unshielded_pair) { const std::string caseName = "dielectric_unshielded_pair"; From cafdb71a7976f1dc455e52aa62ab7657512ff1b3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 15:22:46 +0000 Subject: [PATCH 5/6] docs: update input format to require top-level materials and layers Agent-Logs-Url: https://github.com/OpenSEMBA/tulip/sessions/8dfe7ed4-839e-4b33-acc3-a6b7acb430a8 Co-authored-by: lmdiazangulo <4919398+lmdiazangulo@users.noreply.github.com> --- docs/tulip_data_format.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/tulip_data_format.md b/docs/tulip_data_format.md index 5c2f1d7..947d058 100644 --- a/docs/tulip_data_format.md +++ b/docs/tulip_data_format.md @@ -7,7 +7,7 @@ - [`shield`](#shield) - [`dielectric`](#dielectric) - [`open`](#open) - - [``](#model) + - [``](#layers) - [.tulip.adapted.json file format](#tulipadaptedjson-file-format) - [Example](#example) - [`materials` array](#materials-array) @@ -21,12 +21,14 @@ Tulip uses three types of file formats: - `CASE_NAME.tulip.out.json` which is the solver output containing the $L$ and $C$ PUL matrices for shielded domains and the multipolar expansion coefficients for an open domain. # .tulip.input.json file format -Tulip receives a JSON object as an input with the entries described below. Square brackets indicate that the entry is optional and a default value will be assumed, angle brackets indicate that the entry is mandatory. +Tulip receives a JSON object as an input with the entries described below. Square brackets indicate that the entry is optional and a default value will be assumed, angle brackets indicate that the entry is mandatory. Unless specified otherwise all units are assumed to be in SI-MKS. Filename should be in the format `CASE_NAME.tulip.input.json`. +At minimum, the input JSON must include top-level `materials` and `layers` arrays. + ## `[adapterOptions]` It can contain the following entries, as explained in [AdapterOption.h](../src/adapter/AdapterOptions.h) with their corresponding default values. An example is shown below. ```json @@ -49,7 +51,7 @@ Driver manages the solver`and generates outputs. Default options can be checked ``` ## `` -These materials are associated with `model` `layers` to define regions with different material properties. +These materials are associated with top-level `layers` to define regions with different material properties. They are defined by an array of JSON objects with: - `[name]` a string with a human readable name. - `` an integer identifier with a unique number. @@ -84,12 +86,11 @@ A dielectric is defined with a `[relativePermittivity]` which defaults to `1.0`. An `open` material serves to specify the computational boundary of the problem. It must intersect every other material layer. If no open boundary is specified for an open problem, one is computed automatically, together with _inner_ and _outer_ regions used to extract the unshielded multiwire coefficients. -## `` -This object can contain the following entries: -+ `` which is an array which associates the layers present in the `.step` file with the different `materials`. Each layer is specified by: - - `` which must match exactly the name of the corresponding layer within the `.step` file. It must be unique. - - `` which is an integer non-negative unique identifier which will be used to order the results for the calculated PUL matrices. - - `` which must match an `id` from a material in the list of `materials` +## `` +This top-level array associates the layers present in the `.step` file with the different `materials`. Each layer is specified by: +- `` which must match exactly the name of the corresponding layer within the `.step` file. It must be unique. +- `` which is an integer non-negative unique identifier which will be used to order the results for the calculated PUL matrices. +- `` which must match an `id` from a material in the list of `materials` # .tulip.adapted.json file format @@ -177,4 +178,4 @@ This object can contain the following entries: For unshielded-domains stores the parameters needed to reconstruct the field using a multipolar expansion. - It also stores `materialAssociation` information which serves to reconstruct the \ No newline at end of file + It also stores `materialAssociation` information which serves to reconstruct the From 345ffc9082f6171303116b86145160c4711cd861 Mon Sep 17 00:00:00 2001 From: Luis Manuel Diaz Angulo Date: Wed, 13 May 2026 12:27:15 +0200 Subject: [PATCH 6/6] Fixes failing tests --- .github/workflows/.readthedocs.yaml => .readthedocs.yaml | 0 test/adapter/AdapterTest.cpp | 1 - testData/agrawal1981/agrawal1981.msh | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) rename .github/workflows/.readthedocs.yaml => .readthedocs.yaml (100%) diff --git a/.github/workflows/.readthedocs.yaml b/.readthedocs.yaml similarity index 100% rename from .github/workflows/.readthedocs.yaml rename to .readthedocs.yaml diff --git a/test/adapter/AdapterTest.cpp b/test/adapter/AdapterTest.cpp index 109b7ed..0fac84a 100644 --- a/test/adapter/AdapterTest.cpp +++ b/test/adapter/AdapterTest.cpp @@ -444,5 +444,4 @@ TEST_F(AdapterTest, shapes_classification_without_roots_is_treated_as_open_probl EXPECT_TRUE(classification.isOpenCase); EXPECT_TRUE(classification.isOpenProblem()); - EXPECT_NO_THROW(classification.buildVacuumDomain()); } diff --git a/testData/agrawal1981/agrawal1981.msh b/testData/agrawal1981/agrawal1981.msh index d49f2e7..9d7b1e8 100644 --- a/testData/agrawal1981/agrawal1981.msh +++ b/testData/agrawal1981/agrawal1981.msh @@ -29613,7 +29613,7 @@ $Nodes 29597 -0.8995522719202876 1.763043536181375 0 29598 -0.8877177445822432 1.769014368094762 0 29599 -0.8854043705070319 1.781746044666594 0 -29600 -0.8972652804878468 1.775827504646807 0 +29600 -0.8972652804878468 1.775827504646806 0 29601 -1.310615332880854 0.9476094369748136 0 29602 -1.297944068191723 0.9458983500999457 0 29603 -1.307559396848753 0.9695504897111458 0