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/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 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/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..0fac84a 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"; @@ -395,3 +428,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()); +} 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