/* SPDX-FileCopyrightText: Copyright (c) 2021 - 2025, the adamantine authors. * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #include #include #include #include #include #include namespace adamantine { void validate_input_database(boost::property_tree::ptree &database) { // Tree: physics // PropertyTreeInput physics.thermal bool const use_thermal_physics = database.get("physics.thermal"); // PropertyTreeInput physics.mechanical bool const use_mechanical_physics = database.get("physics.mechanical"); ASSERT_THROW(use_thermal_physics || use_mechanical_physics, "Both thermal and mechanical physics are disabled"); // Tree: boundary // Read the boundary condition type and check for disallowed combinations. // Store if we use radiative or convective boundary so we can check later that // the appropriate material properties are saved. BoundaryType boundary_type = BoundaryType::invalid; // Parse the string std::string delimiter = ","; auto parse_boundary_type = [&](std::string const &boundary, BoundaryType &line_boundary_type, bool &thermal_bc, bool &mechanical_bc) { if (boundary == "adiabatic") { ASSERT_THROW(!(line_boundary_type & BoundaryType::radiative) || !(line_boundary_type & BoundaryType::convective), "adiabatic condition cannot be combined with another type " "of thermal boundary condition."); line_boundary_type |= BoundaryType::adiabatic; thermal_bc = true; } else { if (boundary == "radiative") { ASSERT_THROW(!(line_boundary_type & BoundaryType::adiabatic), "adiabatic condition cannot be combined with another type " "of thermal boundary condition."); line_boundary_type |= BoundaryType::radiative; boundary_type |= BoundaryType::radiative; thermal_bc = true; } else if (boundary == "convective") { ASSERT_THROW(!(line_boundary_type & BoundaryType::adiabatic), "adiabatic condition cannot be combined with another type " "of thermal boundary condition."); line_boundary_type |= BoundaryType::convective; boundary_type |= BoundaryType::convective; thermal_bc = true; } else if (boundary == "clamped") { ASSERT_THROW( !(line_boundary_type & BoundaryType::traction_free), "Mechanical boundary conditions cannot be combined together."); line_boundary_type |= BoundaryType::clamped; mechanical_bc = true; } else if (boundary == "traction_free") { ASSERT_THROW( !(line_boundary_type & BoundaryType::clamped), "Mechanical boundary conditions cannot be combined together."); line_boundary_type |= BoundaryType::traction_free; mechanical_bc = true; } else { ASSERT_THROW(false, "Unknown boundary type."); } } }; auto const boundary_database = database.get_child("boundary"); bool clamped_boundary = false; for (auto const &child_pair : boundary_database) { size_t pos_str = 0; bool thermal_bc = false; bool mechanical_bc = false; BoundaryType line_boundary_type = BoundaryType::invalid; std::string boundary_type_str = "invalid"; if (child_pair.first == "type") { boundary_type_str = child_pair.second.data(); } else { boundary_type_str = boundary_database.get(child_pair.first + ".type"); } while ((pos_str = boundary_type_str.find(delimiter)) != std::string::npos) { std::string boundary = boundary_type_str.substr(0, pos_str); parse_boundary_type(boundary, line_boundary_type, thermal_bc, mechanical_bc); boundary_type_str.erase(0, pos_str + delimiter.length()); } parse_boundary_type(boundary_type_str, line_boundary_type, thermal_bc, mechanical_bc); if (line_boundary_type & BoundaryType::clamped) clamped_boundary = true; if (use_thermal_physics) { ASSERT_THROW(thermal_bc, "Missing thermal boundary condition."); } if (use_mechanical_physics) { ASSERT_THROW(mechanical_bc, "Missing mechanical boundary condition."); } } if (use_mechanical_physics) { ASSERT_THROW(clamped_boundary, "At least one boundary needs to be clamped!"); } // Tree: discretization.thermal if (use_thermal_physics) { // PropertyTreeInput discretization.thermal.fe_degree unsigned int const fe_degree = database.get("discretization.thermal.fe_degree"); ASSERT_THROW(fe_degree > 0 && fe_degree < 6, "fe_degree should be between 1 and 5."); // PropertyTreeInput discretization.thermal.quadrature boost::optional quadrature_type_optional = database.get_optional("discretization.thermal.quadrature"); if (quadrature_type_optional) { std::string quadrature_type = quadrature_type_optional.get(); if (!((boost::iequals(quadrature_type, "gauss") || (boost::iequals(quadrature_type, "lobatto"))))) { ASSERT_THROW(false, "Unknown quadrature type."); } } } // Tree: geometry unsigned int dim = database.get("geometry.dim"); ASSERT_THROW((dim == 2) || (dim == 3), "dim should be 2 or 3"); bool use_powder = database.get("geometry.use_powder", false); if (use_powder) { double powder_layer = database.get("geometry.powder_layer"); ASSERT_THROW(powder_layer >= 0.0, "powder_layer must be non-negative."); } bool material_deposition = database.get("geometry.material_deposition", false); if (material_deposition) { std::string method = database.get("geometry.material_deposition_method"); ASSERT_THROW( (method == "file" || method == "scan_paths"), "Method type for material deposition, '" + method + "', is not recognized. Valid options are: 'file' and 'scan_paths'"); if (method == "file") { ASSERT_THROW(database.count("geometry.material_deposition_file") != 0, "If the material deposition method is 'file', " "'material_deposition_file' must be given."); } else { ASSERT_THROW(database.get_child("geometry").count("deposition_length") != 0, "If the material deposition method is 'scan_path', " "'deposition_length' must be given."); ASSERT_THROW(database.get_child("geometry").count("deposition_height") != 0, "If the material deposition method is 'scan_path', " "'deposition_height' must be given."); if (dim == 3) { ASSERT_THROW(database.get_child("geometry").count("deposition_width") != 0, "If the material deposition method is 'scan_path', " "'deposition_width' must be given."); } ASSERT_THROW( database.get_child("geometry").count("deposition_lead_time") != 0, "If the material deposition method is 'scan_path', " "'deposition_lead_time' must be given."); } } bool import_mesh = database.get("geometry.import_mesh"); if (import_mesh) { ASSERT_THROW(database.get_child("geometry").count("mesh_file") != 0, "If the the mesh is imported, 'mesh_file' must be given."); ASSERT_THROW(database.get_child("geometry").count("mesh_format") != 0, "If the the mesh is imported, 'mesh_format' must be given."); } else { ASSERT_THROW(database.get_child("geometry").count("length") != 0, "If the the mesh is not imported, 'length' must be given."); ASSERT_THROW(database.get_child("geometry").count("height") != 0, "If the the mesh is not imported, 'height' must be given."); if (dim == 3) { ASSERT_THROW(database.get_child("geometry").count("width") != 0, "If the the mesh is not imported, 'width' must be given."); } } // Tree: materials unsigned int n_materials = database.get("materials.n_materials"); std::string property_format = database.get("materials.property_format"); ASSERT_THROW((property_format == "table") || (property_format == "polynomial"), "property_format should be table or polynomial."); for (dealii::types::material_id id = 0; id < n_materials; ++id) { ASSERT_THROW(database.get_child("materials") .count("material_" + std::to_string(id)) != 0, "Number of material subtrees does not match the set number of " "materials."); bool has_a_valid_state = false; for (unsigned int state_index = 0; state_index < material_state_names.size(); ++state_index) { if (database.get_child("materials") .get_child("material_" + std::to_string(id)) .count(material_state_names.at(state_index)) != 0) { has_a_valid_state = true; ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("density") != 0, "Each state needs a user-specified density."); ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("specific_heat") != 0, "Each state needs a user-specified specific heat."); ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("thermal_conductivity_x") != 0, "Each state needs a user-specified specific thermal " "conductivity x."); ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("thermal_conductivity_z") != 0, "Each state needs a user-specified specific thermal " "conductivity z."); if (dim == 3) { ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("thermal_conductivity_y") != 0, "Each state needs a user-specified specific thermal " "conductivity y."); } if (boundary_type & BoundaryType::convective) { ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("convection_heat_transfer_coef") != 0, "Convective BCs require a user-specified convection " "heat transfer coefficient."); } if (boundary_type & BoundaryType::radiative) { ASSERT_THROW(database.get_child("materials") .get_child("material_" + std::to_string(id)) .get_child(material_state_names.at(state_index)) .count("emissivity") != 0, "Radiative BCs require a user-specified emissivity."); } // For now I'm leaving the error checking for the polynomials and tables // for individual properties to the MaterialProperty class. I don't // think it makes sense to duplicate that logic here. } } ASSERT_THROW( has_a_valid_state == true, "Material without any valid state (solid, powder, or liquid)."); if (boundary_type & BoundaryType::convective) { ASSERT_THROW( database.get_child("materials") .get_child("material_" + std::to_string(id)) .count("convection_temperature_infty") != 0, "Convective BCs require setting 'convection_temperature_infty'."); } if (boundary_type & BoundaryType::radiative) { ASSERT_THROW( database.get_child("materials") .get_child("material_" + std::to_string(id)) .count("radiation_temperature_infty") != 0, "Radiative BCs require setting 'radiation_temperature_infty'."); } } // Tree: memory_space boost::optional memory_space_optional = database.get_optional("memory_space"); if (memory_space_optional) { std::string memory_space = memory_space_optional.get(); ASSERT_THROW( (memory_space == "device" || memory_space == "host"), "Method type for memory space, '" + memory_space + "', is not recognized. Valid options are: 'host' and 'device'"); } // Tree: post_processor ASSERT_THROW(database.get_child("post_processor").count("filename_prefix") != 0, "The filename prefix for the postprocessor must be specified."); // Tree: refinement ASSERT_THROW(database.count("refinement") != 0, "A refinement section of the input file must exist."); // Tree: sources unsigned int n_beams = database.get("sources.n_beams"); for (unsigned int beam_index = 0; beam_index < n_beams; ++beam_index) { std::string beam_type = database.get( "sources.beam_" + std::to_string(beam_index) + ".type"); ASSERT_THROW(boost::iequals(beam_type, "goldak") || boost::iequals(beam_type, "electron_beam") || boost::iequals(beam_type, "cube") || boost::iequals(beam_type, "gaussian"), "Beam type, '" + beam_type + "', is not recognized. Valid options are: 'goldak', " "'electron_beam', 'cube', and 'gaussian'."); ASSERT_THROW(database.get_child("sources") .get_child("beam_" + std::to_string(beam_index)) .count("scan_path_file") != 0, "A scan path file for each beam must be given."); std::string file_format = database.get("sources.beam_" + std::to_string(beam_index) + ".scan_path_file_format"); ASSERT_THROW(boost::iequals(file_format, "segment") || boost::iequals(file_format, "event_series"), "Scan path file format, '" + file_format + "', is not recognized. Valid options are: 'segment' and " "'event_series'."); ASSERT_THROW(database.get("sources.beam_" + std::to_string(beam_index) + ".depth") >= 0.0, "Heat source depth must be non-negative."); double absorption_efficiency = database.get("sources.beam_" + std::to_string(beam_index) + ".absorption_efficiency"); ASSERT_THROW(absorption_efficiency >= 0.0 && absorption_efficiency <= 1.0, "Heat source absorption efficiency must be between 0 and 1."); } // Tree: time_stepping std::string time_stepping_method = database.get("time_stepping.method"); ASSERT_THROW(boost::iequals(time_stepping_method, "forward_euler") || boost::iequals(time_stepping_method, "rk_third_order") || boost::iequals(time_stepping_method, "rk_fourth_order"), "Time stepping method, '" + time_stepping_method + "', is not recognized. Valid options are: 'forward_euler', " "'rk_third_order', and 'rk_fourth_order'."); if (database.get("time.scan_path_for_duration", false)) { ASSERT_THROW(database.get("time_stepping.duration") >= 0.0, "Time stepping duration must be non-negative."); } ASSERT_THROW(database.get("time_stepping.time_step") >= 0.0, "Time step must be non-negative."); // Tree: experiment // I'm not checking for the existence of the experimental files here, that's // still done in `adamantine::read_experimental_data_point_cloud` and // `adamantine::RayTracing`. boost::optional experiment_optional_database = database.get_child_optional("experiment"); if (experiment_optional_database) { bool experiment_active = database.get("experiment.read_in_experimental_data", false); if (experiment_active) { ASSERT_THROW(database.get_child("experiment").count("file") != 0, "If reading experimental data, a file must be given."); ASSERT_THROW(database.get_child("experiment").count("last_frame") != 0, "If reading experimental data, a last frame index " "must be given."); std::string experiment_format = database.get("experiment.format"); ASSERT_THROW(boost::iequals(experiment_format, "point_cloud") || boost::iequals(experiment_format, "ray"), "Experiment format must be 'point_cloud' or 'ray'."); unsigned int first_frame_index = database.get("experiment.first_frame", 0); unsigned int last_frame_index = database.get("experiment.last_frame"); ASSERT_THROW(last_frame_index >= first_frame_index, "When reading experimental data, the last frame index " "cannot be lower than the first frame index."); unsigned int first_camera_id = database.get("experiment.first_camera_id"); unsigned int last_camera_id = database.get("experiment.last_camera_id"); ASSERT_THROW(last_camera_id >= first_camera_id, "When reading experimental data, the last camera id cannot " "be lower than the first camera id."); ASSERT_THROW( database.get_child("experiment").count("log_filename") != 0, "If reading experimental data, a log filename must be given."); } } // Tree: ensemble // We check the input in ensemble_management.cc // Tree: data_assimilation boost::optional convergence_tolerance = database.get_optional("data_assimilation.convergence_tolerance"); if (convergence_tolerance) { ASSERT_THROW( convergence_tolerance.get() >= 0.0, "The data assimilation convergene tolerance must be non-negative."); } std::string localization_cutoff_function_str = database.get("data_assimilation.localization_cutoff_function", "none"); if (!(boost::iequals(localization_cutoff_function_str, "gaspari_cohn") || boost::iequals(localization_cutoff_function_str, "step_function") || boost::iequals(localization_cutoff_function_str, "none"))) { ASSERT_THROW(false, "Unknown localization cutoff function. Valid options " "are 'gaspari_cohn', 'step_function', and 'none'."); } // Tree: units boost::optional mesh_unit = database.get_optional("units.mesh"); if (mesh_unit && (!(boost::iequals(mesh_unit.get(), "millimeter") || boost::iequals(mesh_unit.get(), "centimeter") || boost::iequals(mesh_unit.get(), "inch") || boost::iequals(mesh_unit.get(), "meter")))) { ASSERT_THROW(false, "Unknown unit associated with the mesh. Valid options " "are `millimeter`, `centimeter`, `inch`, and `meter`"); } boost::optional heat_source_power_unit = database.get_optional("units.heat_source.power"); if (heat_source_power_unit && (!(boost::iequals(heat_source_power_unit.get(), "milliwatt") || boost::iequals(heat_source_power_unit.get(), "watt")))) { ASSERT_THROW(false, "Unknown unit associated with the power of the heat " "source. Valid options are `milliwatt`, and `watt`"); } boost::optional heat_source_velocity_unit = database.get_optional("units.heat_source.velocity"); if (heat_source_velocity_unit && (!(boost::iequals(heat_source_velocity_unit.get(), "millimeter/second") || boost::iequals(heat_source_velocity_unit.get(), "centimeter/second") || boost::iequals(heat_source_velocity_unit.get(), "meter/second")))) { ASSERT_THROW(false, "Unknown unit associated with the velocity of the heat " "source. Valid options are `millimeter/second`, " "`centimeter/second`, and `meter/second`"); } boost::optional heat_source_dimension_unit = database.get_optional("units.heat_source.dimension"); if (heat_source_dimension_unit && (!(boost::iequals(heat_source_dimension_unit.get(), "millimeter") || boost::iequals(heat_source_dimension_unit.get(), "centimeter") || boost::iequals(heat_source_dimension_unit.get(), "inch") || boost::iequals(heat_source_dimension_unit.get(), "meter")))) { ASSERT_THROW( false, "Unknown unit associated with the dimension of the heat source. Valid " "options are `millimeter`, `centimeter`, `inch`, and `meter`"); } boost::optional heat_source_scan_path_unit = database.get_optional("units.heat_source.scan_path"); if (heat_source_scan_path_unit && (!(boost::iequals(heat_source_scan_path_unit.get(), "millimeter") || boost::iequals(heat_source_scan_path_unit.get(), "centimeter") || boost::iequals(heat_source_scan_path_unit.get(), "inch") || boost::iequals(heat_source_scan_path_unit.get(), "meter")))) { ASSERT_THROW(false, "Unknown unit associated with the scan path. Valid options " "are `millimeter`, `centimeter`, `inch`, and `meter`"); } } } // namespace adamantine