/* SPDX-FileCopyrightText: Copyright (c) 2016 - 2026, the adamantine authors. * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception */ #ifndef ADAMANTINE_HH #define ADAMANTINE_HH #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if DEAL_II_VERSION_GTE(9, 7, 0) && defined(DEAL_II_TRILINOS_WITH_TPETRA) #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ADAMANTINE_WITH_CALIPER #include #endif #include #include template ::value, int> = 0> void output_pvtu( adamantine::PostProcessor &post_processor, unsigned int n_time_step, double time, std::unique_ptr< adamantine::ThermalPhysicsInterface> const &thermal_physics, dealii::LinearAlgebra::distributed::Vector &temperature, std::unique_ptr> const &mechanical_physics, dealii::LA::distributed::Vector &displacement, adamantine::MaterialProperty const &material_properties, std::vector &timers) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif timers[adamantine::output].start(); if (thermal_physics) { thermal_physics->get_affine_constraints().distribute(temperature); if (mechanical_physics) { mechanical_physics->get_affine_constraints().distribute(displacement); post_processor.template write_output::array_layout>( n_time_step, time, temperature, displacement, mechanical_physics->get_stress_tensor(), material_properties.get_state(), material_properties.get_dofs_map(), material_properties.get_dof_handler()); } else { post_processor.template write_thermal_output::array_layout>( n_time_step, time, temperature, material_properties.get_state(), material_properties.get_dofs_map(), material_properties.get_dof_handler()); } } else { mechanical_physics->get_affine_constraints().distribute(displacement); post_processor.template write_mechanical_output::array_layout>( n_time_step, time, displacement, mechanical_physics->get_stress_tensor(), material_properties.get_state(), material_properties.get_dofs_map(), material_properties.get_dof_handler()); } timers[adamantine::output].stop(); } template ::value, int> = 0> void output_pvtu( adamantine::PostProcessor &post_processor, unsigned int n_time_step, double time, std::unique_ptr< adamantine::ThermalPhysicsInterface> const &thermal_physics, dealii::LinearAlgebra::distributed::Vector &temperature, std::unique_ptr> const &mechanical_physics, dealii::LA::distributed::Vector &displacement, adamantine::MaterialProperty const &material_properties, std::vector &timers) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif timers[adamantine::output].start(); auto state = material_properties.get_state(); auto state_host = Kokkos::create_mirror_view_and_copy( dealii::MemorySpace::Host::kokkos_space{}, state); if (thermal_physics) { dealii::LinearAlgebra::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); thermal_physics->get_affine_constraints().distribute(temperature_host); if (mechanical_physics) { mechanical_physics->get_affine_constraints().distribute(displacement); post_processor.template write_output::array_layout>( n_time_step, time, temperature_host, displacement, mechanical_physics->get_stress_tensor(), state_host, material_properties.get_dofs_map(), material_properties.get_dof_handler()); } else { post_processor.template write_thermal_output::array_layout>( n_time_step, time, temperature_host, state_host, material_properties.get_dofs_map(), material_properties.get_dof_handler()); } } else { mechanical_physics->get_affine_constraints().distribute(displacement); post_processor.template write_mechanical_output::array_layout>( n_time_step, time, displacement, mechanical_physics->get_stress_tensor(), state_host, material_properties.get_dofs_map(), material_properties.get_dof_handler()); } timers[adamantine::output].stop(); } // inlining this function so we can have in the header inline void initialize_timers(MPI_Comm const &communicator, std::vector &timers) { timers.push_back(adamantine::Timer(communicator, "Main")); timers.push_back(adamantine::Timer(communicator, "Refinement")); timers.push_back(adamantine::Timer(communicator, "Add Material, Search")); timers.push_back(adamantine::Timer(communicator, "Add Material, Activate")); timers.push_back( adamantine::Timer(communicator, "Data Assimilation, Exp. Data")); timers.push_back( adamantine::Timer(communicator, "Data Assimilation, DOF Mapping")); timers.push_back( adamantine::Timer(communicator, "Data Assimilation, Cov. Sparsity")); timers.push_back( adamantine::Timer(communicator, "Data Assimilation, Exp. Cov.")); timers.push_back( adamantine::Timer(communicator, "Data Assimilation, Update Ensemble")); timers.push_back(adamantine::Timer(communicator, "Evolve One Time Step")); timers.push_back(adamantine::Timer( communicator, "Evolve One Time Step: evaluate_thermal_physics")); timers.push_back(adamantine::Timer( communicator, "Evolve One Time Step: evaluate_material_properties")); timers.push_back(adamantine::Timer(communicator, "Output")); } template std::unique_ptr> initialize( MPI_Comm const &communicator, boost::property_tree::ptree const &database, adamantine::Geometry &geometry, adamantine::Boundary const &boundary, adamantine::MaterialProperty &material_properties) { return std::make_unique>(communicator, database, geometry, boundary, material_properties); } template std::unique_ptr> initialize_quadrature( std::string const &quadrature_type, MPI_Comm const &communicator, boost::property_tree::ptree const &database, adamantine::Geometry &geometry, adamantine::Boundary const &boundary, adamantine::MaterialProperty &material_properties) { if (quadrature_type.compare("gauss") == 0) return initialize>( communicator, database, geometry, boundary, material_properties); else { adamantine::ASSERT_THROW(quadrature_type.compare("lobatto") == 0, "quadrature should be Gauss or Lobatto."); return initialize>( communicator, database, geometry, boundary, material_properties); } } template std::unique_ptr> initialize_thermal_physics( unsigned int fe_degree, std::string const &quadrature_type, MPI_Comm const &communicator, boost::property_tree::ptree const &database, adamantine::Geometry &geometry, adamantine::Boundary const &boundary, adamantine::MaterialProperty &material_properties) { switch (fe_degree) { case 1: { return initialize_quadrature(quadrature_type, communicator, database, geometry, boundary, material_properties); } case 2: { return initialize_quadrature(quadrature_type, communicator, database, geometry, boundary, material_properties); } case 3: { return initialize_quadrature(quadrature_type, communicator, database, geometry, boundary, material_properties); } case 4: { return initialize_quadrature(quadrature_type, communicator, database, geometry, boundary, material_properties); } default: { adamantine::ASSERT_THROW(fe_degree == 5, "fe_degree should be between 1 and 5."); return initialize_quadrature(quadrature_type, communicator, database, geometry, boundary, material_properties); } } } template void refine_and_transfer( std::unique_ptr> &thermal_physics, std::unique_ptr> &mechanical_physics, adamantine::MaterialProperty &material_properties, dealii::DoFHandler &dof_handler, dealii::LA::distributed::Vector &solution) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif dealii::parallel::distributed::Triangulation &triangulation = dynamic_cast &>( const_cast &>( dof_handler.get_triangulation())); // Update the material state from the ThermalOperator to MaterialProperty // because, for now, we need to use state from MaterialProperty to perform the // transfer to the refined mesh. thermal_physics->set_state_to_material_properties(); // Transfer of the solution #if DEAL_II_VERSION_GTE(9, 7, 0) dealii::SolutionTransfer< #else dealii::parallel::distributed::SolutionTransfer< #endif dim, dealii::LA::distributed::Vector> solution_transfer(dof_handler); // Transfer material state unsigned int const direction_data_size = 2; unsigned int const phase_history_data_size = 1; unsigned int constexpr n_material_states = MaterialStates::n_material_states; std::vector> data_to_transfer; std::vector dummy_cell_data(n_material_states + direction_data_size + phase_history_data_size, std::numeric_limits::infinity()); auto state_host = Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace{}, material_properties.get_state()); unsigned int cell_id = 0; unsigned int activated_cell_id = 0; for (auto const &cell : dof_handler.active_cell_iterators()) { if (cell->is_locally_owned()) { std::vector cell_data(n_material_states + direction_data_size + phase_history_data_size); for (unsigned int i = 0; i < n_material_states; ++i) cell_data[i] = state_host(i, cell_id); if (cell->active_fe_index() == 0) { cell_data[n_material_states] = thermal_physics->get_deposition_cos(activated_cell_id); cell_data[n_material_states + 1] = thermal_physics->get_deposition_sin(activated_cell_id); if (thermal_physics->get_has_melted(activated_cell_id)) cell_data[n_material_states + direction_data_size] = 1.0; else cell_data[n_material_states + direction_data_size] = 0.0; ++activated_cell_id; } else { cell_data[n_material_states] = std::numeric_limits::infinity(); cell_data[n_material_states + 1] = std::numeric_limits::infinity(); cell_data[n_material_states + direction_data_size] = std::numeric_limits::infinity(); } data_to_transfer.push_back(cell_data); ++cell_id; } else { data_to_transfer.push_back(dummy_cell_data); } } // Prepare the Triangulation and the diffent data transfer objects for // refinement triangulation.prepare_coarsening_and_refinement(); // Prepare for refinement of the solution dealii::LA::distributed::Vector solution_host; if constexpr (std::is_same_v) { // We need to apply the constraints before the mesh transfer thermal_physics->get_affine_constraints().distribute(solution); // We need to update the ghost values before we can do the interpolation on // the new mesh. solution.update_ghost_values(); solution_transfer.prepare_for_coarsening_and_refinement(solution); } else { solution_host.reinit(solution.get_partitioner()); solution_host.import_elements(solution, dealii::VectorOperation::insert); // We need to apply the constraints before the mesh transfer thermal_physics->get_affine_constraints().distribute(solution_host); // We need to update the ghost values before we can do the interpolation on // the new mesh. solution_host.update_ghost_values(); solution_transfer.prepare_for_coarsening_and_refinement(solution_host); } dealii::parallel::distributed::CellDataTransfer< dim, dim, std::vector>> cell_data_trans(triangulation); cell_data_trans.prepare_for_coarsening_and_refinement(data_to_transfer); if (mechanical_physics) { mechanical_physics->prepare_transfer_mpi(); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("refine triangulation"); #endif // Execute the refinement triangulation.execute_coarsening_and_refinement(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("refine triangulation"); #endif // Update the AffineConstraints and resize the solution thermal_physics->setup_dofs(); thermal_physics->initialize_dof_vector(0., solution); // Update MaterialProperty DoFHandler and resize the state vectors material_properties.reinit_dofs(); // Interpolate the solution if constexpr (std::is_same_v) { solution_transfer.interpolate(solution); } else { solution_host.reinit(solution.get_partitioner()); solution_transfer.interpolate(solution_host); solution.import_elements(solution_host, dealii::VectorOperation::insert); } // Unpack the material state and repopulate the material state std::vector> transferred_data( triangulation.n_active_cells(), std::vector(n_material_states + direction_data_size + phase_history_data_size)); cell_data_trans.unpack(transferred_data); auto state = material_properties.get_state(); state_host = Kokkos::create_mirror_view(state); unsigned int total_cell_id = 0; cell_id = 0; std::vector transferred_cos; std::vector transferred_sin; std::vector has_melted; for (auto const &cell : dof_handler.active_cell_iterators()) { if (cell->is_locally_owned()) { for (unsigned int i = 0; i < n_material_states; ++i) { state_host(i, cell_id) = transferred_data[total_cell_id][i]; } if (cell->active_fe_index() == 0) { transferred_cos.push_back( transferred_data[total_cell_id][n_material_states]); transferred_sin.push_back( transferred_data[total_cell_id][n_material_states + 1]); // Convert from double back to bool if (transferred_data[total_cell_id] [n_material_states + direction_data_size] > 0.5) has_melted.push_back(true); else has_melted.push_back(false); } ++cell_id; } ++total_cell_id; } // Update the deposition cos and sin thermal_physics->set_material_deposition_orientation(transferred_cos, transferred_sin); // Update the melted indicator thermal_physics->set_has_melted_vector(has_melted); // Copy the data back to material_property Kokkos::deep_copy(state, state_host); // Update the material states in the ThermalOperator thermal_physics->get_state_from_material_properties(); #if ADAMANTINE_DEBUG // Check that we are not losing material cell_id = 0; for (auto const &cell : dof_handler.active_cell_iterators()) { if (cell->is_locally_owned()) { double material_ratio = 0.; for (unsigned int i = 0; i < n_material_states; ++i) { material_ratio += state_host(i, cell_id); } ASSERT(std::abs(material_ratio - 1.) < 1e-14, "Material is lost."); ++cell_id; } } #endif if (mechanical_physics) { mechanical_physics->complete_transfer_mpi(); } } template std::vector::active_cell_iterator> compute_cells_to_refine( dealii::parallel::distributed::Triangulation &triangulation, double const time, double const next_refinement_time, unsigned int const n_time_steps, std::vector>> const &heat_sources) { // Compute the position of the beams between time and next_refinement_time and // create a list of cells that will intersect the beam. // Build the bounding boxes associated with the locally owned cells std::vector> cell_bounding_boxes; cell_bounding_boxes.reserve(triangulation.n_locally_owned_active_cells()); for (auto const &cell : triangulation.active_cell_iterators() | dealii::IteratorFilters::LocallyOwnedCell()) { cell_bounding_boxes.push_back(cell->bounding_box()); } dealii::ArborXWrappers::BVH bvh(cell_bounding_boxes); double const bounding_box_scaling = 2.0; std::vector> heat_source_bounding_boxes; heat_source_bounding_boxes.reserve(n_time_steps * heat_sources.size()); for (unsigned int i = 0; i < n_time_steps; ++i) { double const current_time = time + static_cast(i) / static_cast(n_time_steps) * (next_refinement_time - time); // Build the bounding boxes associated with the heat sources for (auto &beam : heat_sources) { heat_source_bounding_boxes.push_back( beam->get_bounding_box(current_time, bounding_box_scaling)); } } // Perform the search with ArborX. Since we are only interested in locally // owned cells, we use BVH. dealii::ArborXWrappers::BoundingBoxIntersectPredicate bb_intersect( heat_source_bounding_boxes); auto [indices, offset] = bvh.query(bb_intersect); // Put the indices into a set to get rid of the duplicates and to make it // easier to check if the indices are found. std::unordered_set indices_to_refine; indices_to_refine.insert(indices.begin(), indices.end()); std::vector::active_cell_iterator> cells_to_refine; int cell_index = 0; for (auto const &cell : triangulation.active_cell_iterators() | dealii::IteratorFilters::LocallyOwnedCell()) { if (indices_to_refine.count(cell_index)) { cells_to_refine.push_back(cell); } ++cell_index; } return cells_to_refine; } template void refine_mesh( std::unique_ptr> &thermal_physics, std::unique_ptr> &mechanical_physics, adamantine::MaterialProperty &material_properties, dealii::LA::distributed::Vector &solution, std::vector>> const &heat_sources, double const time, double const next_refinement_time, unsigned int const time_steps_refinement, boost::property_tree::ptree const &refinement_database) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif dealii::DoFHandler &dof_handler = thermal_physics->get_dof_handler(); dealii::parallel::distributed::Triangulation &triangulation = dynamic_cast &>( const_cast &>( dof_handler.get_triangulation())); // PropertyTreeInput refinement.n_refinements unsigned int const n_refinements = refinement_database.get("n_refinements", 2); for (unsigned int i = 0; i < n_refinements; ++i) { // Compute the cells to be refined. auto cells_to_refine = compute_cells_to_refine(triangulation, time, next_refinement_time, time_steps_refinement, heat_sources); // PropertyTreeInput refinement.coarsen_after_beam const bool coarsen_after_beam = refinement_database.get("coarsen_after_beam", false); // If coarsening is allowed, set the coarsening flag everywhere if (coarsen_after_beam) { for (auto cell : dealii::filter_iterators( triangulation.active_cell_iterators(), dealii::IteratorFilters::LocallyOwnedCell())) { if (cell->level() > 0) cell->set_coarsen_flag(); } } // Flag the cells for refinement. for (auto &cell : cells_to_refine) { if (coarsen_after_beam) cell->clear_coarsen_flag(); if (cell->level() < static_cast(n_refinements)) cell->set_refine_flag(); } // Execute the refinement and transfer the solution onto the new mesh. refine_and_transfer(thermal_physics, mechanical_physics, material_properties, dof_handler, solution); } // Recompute the inverse of the mass matrix thermal_physics->compute_inverse_mass_matrix(); } template void refine_mesh( std::unique_ptr> &thermal_physics, std::unique_ptr> &mechanical_physics, adamantine::MaterialProperty &material_properties, dealii::LA::distributed::Vector &solution, std::vector>> const &heat_sources, double const time, double const next_refinement_time, unsigned int const time_steps_refinement, boost::property_tree::ptree const &refinement_database) { if (!thermal_physics) return; switch (thermal_physics->get_fe_degree()) { case 1: { refine_mesh( thermal_physics, mechanical_physics, material_properties, solution, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); break; } case 2: { refine_mesh( thermal_physics, mechanical_physics, material_properties, solution, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); break; } case 3: { refine_mesh( thermal_physics, mechanical_physics, material_properties, solution, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); break; } case 4: { refine_mesh( thermal_physics, mechanical_physics, material_properties, solution, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); break; } case 5: { refine_mesh( thermal_physics, mechanical_physics, material_properties, solution, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); break; } default: { adamantine::ASSERT_THROW(false, "fe_degree should be between 1 and 5."); } } } template std::pair, dealii::LinearAlgebra::distributed::Vector> run(MPI_Comm const &communicator, boost::property_tree::ptree const &database, std::vector &timers) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif // Get optional units property tree boost::optional units_optional_database = database.get_child_optional("units"); // Create the Geometry boost::property_tree::ptree geometry_database = database.get_child("geometry"); adamantine::Geometry geometry(communicator, geometry_database, units_optional_database); // Create the MaterialProperty boost::property_tree::ptree material_database = database.get_child("materials"); adamantine::MaterialProperty material_properties(communicator, geometry.get_triangulation(), material_database); // Extract the physics property tree boost::property_tree::ptree physics_database = database.get_child("physics"); bool const use_thermal_physics = physics_database.get("thermal"); bool const use_mechanical_physics = physics_database.get("mechanical"); // Extract the discretization property tree boost::property_tree::ptree discretization_database = database.get_child("discretization"); // Extract the post-processor property tree boost::property_tree::ptree post_processor_database = database.get_child("post_processor"); // Extract the verbosity // PropertyTreeInput verbose_output bool const verbose_output = database.get("verbose_output", false); // Create the Boundary boost::property_tree::ptree boundary_database = database.get_child("boundary"); adamantine::Boundary boundary( boundary_database, geometry.get_triangulation().get_boundary_ids()); // Create ThermalPhysics if necessary std::unique_ptr> thermal_physics; std::vector>> heat_sources; if (use_thermal_physics) { // PropertyTreeInput discretization.thermal.fe_degree unsigned int const fe_degree = discretization_database.get("thermal.fe_degree"); // PropertyTreeInput discretization.thermal.quadrature std::string quadrature_type = discretization_database.get("thermal.quadrature", "gauss"); thermal_physics = initialize_thermal_physics( fe_degree, quadrature_type, communicator, database, geometry, boundary, material_properties); heat_sources = thermal_physics->get_heat_sources(); post_processor_database.put("thermal_output", true); } // PropertyTreeInput materials.initial_temperature double const initial_temperature = material_database.get("initial_temperature", 300.); // Get the reference temperature(s) for the substrate and deposited // material(s) std::vector material_reference_temps; // PropertyTreeInput materials.n_materials unsigned int const n_materials_runtime = material_database.get("n_materials"); for (unsigned int i = 0; i < n_materials_runtime; ++i) { // Use the solidus as the reference temperature, in the future we may want // to use something else // PropertyTreeInput materials.material_n.solidus double const reference_temperature = material_database.get( "material_" + std::to_string(i) + ".solidus"); material_reference_temps.push_back(reference_temperature); } material_reference_temps.push_back(initial_temperature); // Create MechanicalPhysics std::unique_ptr> mechanical_physics; if (use_mechanical_physics) { // PropertyTreeInput discretization.mechanical.fe_degree unsigned int const fe_degree = discretization_database.get("mechanical.fe_degree"); mechanical_physics = std::make_unique>( communicator, fe_degree, geometry, boundary, material_properties, material_reference_temps); post_processor_database.put("mechanical_output", true); } // Create PostProcessor std::unique_ptr> post_processor; bool thermal_output = post_processor_database.get("thermal_output", false); bool mechanical_output = post_processor_database.get("mechanical_output", false); if ((thermal_output) && (mechanical_output)) { post_processor = std::make_unique>( communicator, post_processor_database, thermal_physics->get_dof_handler(), mechanical_physics->get_dof_handler()); } else { post_processor = std::make_unique>( communicator, post_processor_database, thermal_output ? thermal_physics->get_dof_handler() : mechanical_physics->get_dof_handler()); } // Get the checkpoint and restart subtrees boost::optional checkpoint_optional_database = database.get_child_optional("checkpoint"); boost::optional restart_optional_database = database.get_child_optional("restart"); unsigned int time_steps_checkpoint = std::numeric_limits::max(); std::string checkpoint_filename; bool checkpoint_overwrite = true; if (checkpoint_optional_database) { auto checkpoint_database = checkpoint_optional_database.get(); // PropertyTreeInput checkpoint.time_steps_between_checkpoint time_steps_checkpoint = checkpoint_database.get("time_steps_between_checkpoint"); // PropertyTreeInput checkpoint.filename_prefix checkpoint_filename = checkpoint_database.get("filename_prefix"); // PropertyTreeInput checkpoint.overwrite_files checkpoint_overwrite = checkpoint_database.get("overwrite_files"); } bool restart = false; std::string restart_filename; if (restart_optional_database) { restart = true; auto restart_database = restart_optional_database.get(); // PropertyTreeInput restart.filename_prefix restart_filename = restart_database.get("filename_prefix"); } dealii::LA::distributed::Vector temperature; dealii::LA::distributed::Vector displacement; if (use_thermal_physics) { if (restart == false) { thermal_physics->setup(); thermal_physics->initialize_dof_vector(initial_temperature, temperature); } else { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("restart from file"); #endif thermal_physics->load_checkpoint(restart_filename, temperature); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("restart from file"); #endif } } if (use_mechanical_physics) { // We currently do not support restarting the mechanical simulation. adamantine::ASSERT_THROW( restart == false, "Mechanical simulation cannot be restarted from a file"); if (use_thermal_physics) { // Thermo-mechanical simulation dealii::LA::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); mechanical_physics->setup_dofs( thermal_physics->get_dof_handler(), temperature_host, thermal_physics->get_has_melted_vector(), true); } else { // Mechanical only simulation mechanical_physics->setup_dofs(); } displacement = mechanical_physics->solve(); } unsigned int progress = 0; unsigned int n_time_step = 0; double time = 0.; double activation_time_end = -1.; unsigned int const rank = dealii::Utilities::MPI::this_mpi_process(communicator); if (restart == true) { if (rank == 0) { std::cout << "Restarting from file" << std::endl; } std::ifstream file{restart_filename + "_time.txt"}; boost::archive::text_iarchive ia{file}; ia >> time; ia >> n_time_step; } // PropertyTreeInput geometry.deposition_time double const activation_time = geometry_database.get("deposition_time", 0.); // Output the initial solution output_pvtu(*post_processor, n_time_step, time, thermal_physics, temperature, mechanical_physics, displacement, material_properties, timers); ++n_time_step; // Create the bounding boxes used for material deposition auto [material_deposition_boxes, deposition_times, deposition_cos, deposition_sin] = adamantine::create_material_deposition_boxes(geometry_database, heat_sources); // Extract the time-stepping database boost::property_tree::ptree time_stepping_database = database.get_child("time_stepping"); // PropertyTreeInput time_stepping.time_step double time_step = time_stepping_database.get("time_step"); // PropertyTreeInput time_stepping.mechanical_time_step_factor unsigned int const mechanical_time_step_factor = time_stepping_database.get("mechanical_time_step_factor", 1); // PropertyTreeInput time_stepping.scan_path_for_duration bool const scan_path_for_duration = time_stepping_database.get("scan_path_for_duration", false); // PropertyTreeInput time_stepping.duration double duration = scan_path_for_duration ? std::numeric_limits::max() : time_stepping_database.get("duration"); // Extract the refinement database boost::property_tree::ptree refinement_database = database.get_child("refinement"); // PropertyTreeInput refinement.time_steps_between_refinement unsigned int const time_steps_refinement = refinement_database.get("time_steps_between_refinement", 10); // PropertyTreeInput post_processor.time_steps_between_output unsigned int const time_steps_output = post_processor_database.get("time_steps_between_output", 1); // PropertyTreeInput materials.new_material_temperature double const new_material_temperature = database.get("materials.new_material_temperature", 300.); // Extract the microstructure database if present boost::optional microstructure_optional_database = database.get_child_optional("microstructure"); bool const compute_microstructure = microstructure_optional_database ? true : false; std::unique_ptr> microstructure; if (compute_microstructure) { auto microstructure_database = microstructure_optional_database.get(); // PropertyTreeInput microstructure.filename_prefix std::string microstructure_filename = microstructure_database.get("filename_prefix"); microstructure = std::make_unique>( communicator, microstructure_filename); } bool rebuild_mechanical_matrix = true; // If the duration is derived from the scan path, we get the time of the last // segment. If the scan path file contains SCAN_PATH_END, we set the final // duration. double current_scan_path_end = 0; if (scan_path_for_duration) { bool scan_path_end = true; double current_end_time = std::numeric_limits::max(); for (auto &source : heat_sources) { double scan_path_end_time = source->get_scan_path().get_segment_list().back().end_time; if (current_end_time > scan_path_end_time) current_end_time = scan_path_end_time; if (!source->get_scan_path().is_finished()) scan_path_end = false; } current_scan_path_end = current_end_time; if (scan_path_end) duration = current_scan_path_end; } #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_BEGIN(main_loop_id, "main_loop"); #endif while (time < duration) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_ITERATION(main_loop_id, n_time_step - 1); #endif // If we use scan_path_for_duration, we may need to read the scan path // file once again. // We use an epsilon to get the "expected" behavior when the deposition // time and the time match should match exactly but don't because of // floating point accuracy. double const time_step_tol = time_step * 1e-6; // The condition is written in such a way that it avoids the subtraction of // a large number (current_scan_path_end) with a much smaller one // (time_step_tol). if (scan_path_for_duration && (time - current_scan_path_end > -time_step_tol)) { // Check if we have reached the end of the file. If not, read the // updated scan path file bool scan_path_end = true; double current_end_time = std::numeric_limits::max(); for (auto &source : heat_sources) { if (!source->get_scan_path().is_finished()) { scan_path_end = false; // This functions waits for the scan path file to be updated // before reading the file. source->get_scan_path().read_file(); // Update the current_end_time double scan_path_end_time = source->get_scan_path().get_segment_list().back().end_time; if (current_end_time > scan_path_end_time) current_end_time = scan_path_end_time; } } current_scan_path_end = current_end_time; if (scan_path_end && (current_scan_path_end != std::numeric_limits::max())) duration = current_scan_path_end; std::tie(material_deposition_boxes, deposition_times, deposition_cos, deposition_sin) = adamantine::create_material_deposition_boxes(geometry_database, heat_sources); } if ((time + time_step) > duration) time_step = duration - time; // Refine the mesh the first time we get in the loop and after // time_steps_refinement time steps. if (((n_time_step == 1) || ((n_time_step % time_steps_refinement) == 0)) && use_thermal_physics) { timers[adamantine::refine].start(); double next_refinement_time = time + time_steps_refinement * time_step; refine_mesh(thermal_physics, mechanical_physics, material_properties, temperature, heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); timers[adamantine::refine].stop(); rebuild_mechanical_matrix = true; if ((rank == 0) && (verbose_output == true)) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (thermal) after mesh refinement: " << thermal_physics->get_dof_handler().n_dofs() << std::endl; } } // Add material if necessary. timers[adamantine::add_material_activate].start(); // The condition is written in such a way that it avoids the subtraction of // a large number (activation_time_end) with a much smaller one // (time_step_tol). if (time - activation_time_end > -time_step_tol) { auto activation_start = std::lower_bound(deposition_times.begin(), deposition_times.end(), time - time_step_tol) - deposition_times.begin(); activation_time_end = std::min(time + std::max(activation_time, time_step), duration); auto activation_end = std::lower_bound(deposition_times.begin(), deposition_times.end(), activation_time_end) - deposition_times.begin(); if (activation_start < activation_end) { if (use_thermal_physics) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("add material"); #endif // Compute the elements to activate. // TODO Right now, we compute the list of cells that get activated for // the entire material deposition. We should restrict the list to the // cells that are activated between activation_start and // activation_end. timers[adamantine::add_material_search].start(); auto elements_to_activate = adamantine::get_elements_to_activate( geometry, thermal_physics->get_dof_handler(), material_deposition_boxes); timers[adamantine::add_material_search].stop(); // For now assume that all deposited material has never been melted // (may or may not be reasonable) std::vector has_melted(deposition_cos.size(), false); thermal_physics->add_material_start( elements_to_activate, deposition_cos, deposition_sin, has_melted, activation_start, activation_end, temperature); if (use_mechanical_physics) { mechanical_physics->prepare_transfer_mpi(); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("refine triangulation"); #endif dealii::parallel::distributed::Triangulation &triangulation = dynamic_cast &>( const_cast &>( thermal_physics->get_dof_handler().get_triangulation())); triangulation.execute_coarsening_and_refinement(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("refine triangulation"); #endif thermal_physics->add_material_end(new_material_temperature, temperature); if (use_mechanical_physics) { mechanical_physics->complete_transfer_mpi(); rebuild_mechanical_matrix = true; } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("add material"); #endif } } if ((rank == 0) && (verbose_output == true) && (activation_end - activation_start > 0) && use_thermal_physics) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (thermal) after cell activation: " << thermal_physics->get_dof_handler().n_dofs() << std::endl; } } timers[adamantine::add_material_activate].stop(); // If thermomechanics are being solved, mark cells that are above the // solidus as cells that should have their reference temperature reset. // This cannot be in the thermomechanics solve because some cells may go // above the solidus and then back below the solidus in the time between // thermomechanical solves. if (use_thermal_physics && use_mechanical_physics) { thermal_physics->mark_has_melted(material_reference_temps[0], temperature); } timers[adamantine::evol_time].start(); // Solve the thermal problem if (use_thermal_physics) { if (compute_microstructure) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("compute microstructure"); #endif if constexpr (std::is_same_v) { microstructure->set_old_temperature(temperature); } else { dealii::LA::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); microstructure->set_old_temperature(temperature_host); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("compute microstructure"); #endif } time = thermal_physics->evolve_one_time_step(time, time_step, temperature, timers); if (compute_microstructure) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("compute microstructure"); #endif if constexpr (std::is_same_v) { microstructure->compute_G_and_R(material_properties, thermal_physics->get_dof_handler(), temperature, time_step); } else { dealii::LA::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); microstructure->compute_G_and_R(material_properties, thermal_physics->get_dof_handler(), temperature_host, time_step); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("compute microstructure"); #endif } } // Solve the (thermo-)mechanical problem if (use_mechanical_physics) { // Solve if the time step is a multipe of mechanical_time_step_factor or // if we will output the results. if ((n_time_step % mechanical_time_step_factor == 0) || (n_time_step % time_steps_output == 0)) { if (use_thermal_physics) { // Update the material state thermal_physics->set_state_to_material_properties(); dealii::LA::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); // We need to rebuild the matrix if the mesh has changed, material was // deposited or if the material changed phases. We pass the // information about the mesh to setup_dofs. mechanical_physics->setup_dofs( thermal_physics->get_dof_handler(), temperature_host, thermal_physics->get_has_melted_vector(), rebuild_mechanical_matrix); // We only output the number of dofs if the mesh was modified or if // material was deposited. if ((rank == 0) && (verbose_output == true) && (rebuild_mechanical_matrix == true)) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (mechanical): " << mechanical_physics->get_dof_handler().n_dofs() << std::endl; } rebuild_mechanical_matrix = false; } else { if (rebuild_mechanical_matrix) { mechanical_physics->setup_dofs(); if ((rank == 0) && (verbose_output == true)) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (mechanical): " << mechanical_physics->get_dof_handler().n_dofs() << std::endl; } rebuild_mechanical_matrix = false; } else { mechanical_physics->update_rhs(); } } displacement = mechanical_physics->solve(); } } timers[adamantine::evol_time].stop(); if (n_time_step % time_steps_checkpoint == 0) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("save checkpoint"); #endif if (rank == 0) { std::cout << "Checkpoint reached" << std::endl; } std::string output_dir = post_processor_database.get("output_dir", ""); std::string filename_prefix = checkpoint_overwrite ? checkpoint_filename : checkpoint_filename + '_' + std::to_string(n_time_step); thermal_physics->save_checkpoint(filename_prefix, temperature); std::ofstream file{output_dir + filename_prefix + "_time.txt"}; boost::archive::text_oarchive oa{file}; oa << time; oa << n_time_step; #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("save checkpoint"); #endif } // Output progress on screen if (rank == 0) { double adim_time = time / (duration / 10.); double int_part = 0; std::modf(adim_time, &int_part); if (int_part > progress) { std::cout << int_part * 10 << '%' << " completed" << std::endl; progress = static_cast(int_part); } } // Output the solution if (n_time_step % time_steps_output == 0) { if (use_thermal_physics) { thermal_physics->set_state_to_material_properties(); } output_pvtu(*post_processor, n_time_step, time, thermal_physics, temperature, mechanical_physics, displacement, material_properties, timers); } ++n_time_step; } #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_END(main_loop_id); #endif post_processor->write_pvd(); // This is only used for integration test if constexpr (std::is_same_v) { if (use_thermal_physics) { thermal_physics->get_affine_constraints().distribute(temperature); } if (use_mechanical_physics) { mechanical_physics->get_affine_constraints().distribute(displacement); } return std::make_pair(temperature, displacement); } else { dealii::LinearAlgebra::distributed::Vector temperature_host(temperature.get_partitioner()); temperature_host.import_elements(temperature, dealii::VectorOperation::insert); if (use_thermal_physics) { thermal_physics->get_affine_constraints().distribute(temperature_host); } if (use_mechanical_physics) { mechanical_physics->get_affine_constraints().distribute(displacement); } return std::make_pair(temperature_host, displacement); } } void split_global_communicator(MPI_Comm global_communicator, unsigned int global_ensemble_size, MPI_Comm &local_communicator, unsigned int &local_ensemble_size, unsigned int &first_local_member, int &my_color) { unsigned int const global_rank = dealii::Utilities::MPI::this_mpi_process(global_communicator); unsigned int const global_n_procs = dealii::Utilities::MPI::n_mpi_processes(global_communicator); double const avg_n_procs_per_member = static_cast(global_n_procs) / static_cast(global_ensemble_size); if (avg_n_procs_per_member > 1) { local_ensemble_size = 1; // We need all the members to use the same partitioning otherwise the dofs // may be number differently which is problematic for the data assimulation. adamantine::ASSERT_THROW( avg_n_procs_per_member == std::floor(avg_n_procs_per_member), "Number of MPI ranks should be less than the number of ensemble " "members or a multiple of this number."); // Assign color my_color = global_rank / static_cast(std::floor(avg_n_procs_per_member)); first_local_member = my_color; } else { my_color = global_rank; // Compute the local ensemble size unsigned int const members_per_proc = global_ensemble_size / global_n_procs; unsigned int const leftover_members = global_ensemble_size - global_n_procs * members_per_proc; // We assign all the leftover members to processor 0. We could do a // better job distributing the leftover local_ensemble_size = members_per_proc; if (global_rank == 0) { local_ensemble_size += leftover_members; first_local_member = 0; } else { first_local_member = global_rank * local_ensemble_size + leftover_members; } } MPI_Comm_split(global_communicator, my_color, 0, &local_communicator); } template std::vector> run_ensemble(MPI_Comm const &global_communicator, boost::property_tree::ptree const &database, std::vector &timers) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_FUNCTION; #endif // ------ Extract child property trees ----- // Mandatory subtrees boost::property_tree::ptree geometry_database = database.get_child("geometry"); boost::property_tree::ptree boundary_database = database.get_child("boundary"); boost::property_tree::ptree discretization_database = database.get_child("discretization"); boost::property_tree::ptree time_stepping_database = database.get_child("time_stepping"); boost::property_tree::ptree post_processor_database = database.get_child("post_processor"); boost::property_tree::ptree refinement_database = database.get_child("refinement"); boost::property_tree::ptree material_database = database.get_child("materials"); // Optional subtrees boost::optional units_optional_database = database.get_child_optional("units"); boost::optional experiment_optional_database = database.get_child_optional("experiment"); boost::optional data_assimilation_optional_database = database.get_child_optional("data_assimilation"); // Verbosity // PropertyTreeInput verbose_output bool const verbose_output = database.get("verbose_output", false); // ------ Get finite element implementation parameters ----- // PropertyTreeInput discretization.thermal.fe_degree unsigned int const fe_degree = discretization_database.get("thermal.fe_degree"); // PropertyTreeInput discretization.thermal.quadrature std::string quadrature_type = discretization_database.get("thermal.quadrature", "gauss"); std::transform(quadrature_type.begin(), quadrature_type.end(), quadrature_type.begin(), [](unsigned char c) { return std::tolower(c); }); // ------ Split MPI communicator ----- // PropertyTreeInput ensemble.ensemble_size unsigned int const global_ensemble_size = database.get("ensemble.ensemble_size", 5); // Distribute the processors among the ensemble members MPI_Comm local_communicator; unsigned int local_ensemble_size = std::numeric_limits::max(); unsigned int first_local_member = std::numeric_limits::max(); int my_color = -1; split_global_communicator(global_communicator, global_ensemble_size, local_communicator, local_ensemble_size, first_local_member, my_color); // ------ Set up the ensemble members ----- // There might be a more efficient way to share some of these objects // between ensemble members. For now, we'll do the simpler approach of // duplicating everything. // Create a new property tree database for each ensemble member std::vector database_ensemble = adamantine::create_database_ensemble( database, local_communicator, first_local_member, local_ensemble_size, global_ensemble_size); std::vector>> thermal_physics_ensemble(local_ensemble_size); std::vector>>> heat_sources_ensemble(local_ensemble_size); std::vector>> geometry_ensemble; std::vector> boundary_ensemble; std::vector>> material_properties_ensemble; std::vector>> post_processor_ensemble; // Create the vector of augmented state vectors std::vector> solution_augmented_ensemble(local_ensemble_size); // Give names to the blocks in the augmented state vector int constexpr base_state = 0; int constexpr augmented_state = 1; // ----- Initialize the data assimilation object ----- boost::property_tree::ptree data_assimilation_database; bool assimilate_data = false; std::vector augmented_state_parameters; if (data_assimilation_optional_database) { // PropertyTreeInput data_assimilation data_assimilation_database = data_assimilation_optional_database.get(); // PropertyTreeInput data_assimilation.assimilate_data if (data_assimilation_database.get("assimilate_data", false)) { assimilate_data = true; // PropertyTreeInput data_assimilation.augment_with_beam_0_absorption if (data_assimilation_database.get("augment_with_beam_0_absorption", false)) { augmented_state_parameters.push_back( adamantine::AugmentedStateParameters::beam_0_absorption); } // PropertyTreeInput data_assimilation.augment_with_beam_0_max_power if (data_assimilation_database.get("augment_with_beam_0_max_power", false)) { augmented_state_parameters.push_back( adamantine::AugmentedStateParameters::beam_0_max_power); } } } adamantine::DataAssimilator data_assimilator(global_communicator, local_communicator, my_color, data_assimilation_database); // Get the checkpoint and restart subtrees boost::optional checkpoint_optional_database = database.get_child_optional("checkpoint"); boost::optional restart_optional_database = database.get_child_optional("restart"); unsigned int const global_rank = dealii::Utilities::MPI::this_mpi_process(global_communicator); unsigned int time_steps_checkpoint = std::numeric_limits::max(); std::string checkpoint_filename; bool checkpoint_overwrite = true; if (checkpoint_optional_database) { auto checkpoint_database = checkpoint_optional_database.get(); // PropertyTreeInput checkpoint.time_steps_between_checkpoint time_steps_checkpoint = checkpoint_database.get("time_steps_between_checkpoint"); // PropertyTreeInput checkpoint.filename_prefix checkpoint_filename = checkpoint_database.get("filename_prefix"); // PropertyTreeInput checkpoint.overwrite_files checkpoint_overwrite = checkpoint_database.get("overwrite_files"); } bool restart = false; std::string restart_filename; if (restart_optional_database) { restart = true; auto restart_database = restart_optional_database.get(); // PropertyTreeInput restart.filename_prefix restart_filename = restart_database.get("filename_prefix"); } for (unsigned int member = 0; member < local_ensemble_size; ++member) { // Resize the augmented ensemble block vector to have two blocks solution_augmented_ensemble[member].reinit(2); // Edit the database for the ensemble if (database.get("sources.n_beams") > 0) { // Populate the parameter augmentation block of the augmented state // ensemble if (assimilate_data) { solution_augmented_ensemble[member] .block(augmented_state) .reinit(augmented_state_parameters.size()); for (unsigned int index = 0; index < augmented_state_parameters.size(); ++index) { // FIXME: Need to consider how we want to generalize this. It could // get unwieldy if we want to specify every parameter of an // arbitrary number of beams. if (augmented_state_parameters.at(index) == adamantine::AugmentedStateParameters::beam_0_absorption) { solution_augmented_ensemble[member].block(augmented_state)[index] = database_ensemble[member].get( "sources.beam_0.absorption_efficiency"); } else if (augmented_state_parameters.at(index) == adamantine::AugmentedStateParameters::beam_0_max_power) { solution_augmented_ensemble[member].block(augmented_state)[index] = database_ensemble[member].get( "sources.beam_0.max_power"); } } } } solution_augmented_ensemble[member].collect_sizes(); geometry_ensemble.push_back(std::make_unique>( local_communicator, geometry_database, units_optional_database)); boundary_ensemble.push_back(std::make_unique( boundary_database, geometry_ensemble.back()->get_triangulation().get_boundary_ids())); material_properties_ensemble.push_back( std::make_unique>( local_communicator, geometry_ensemble.back()->get_triangulation(), material_database)); thermal_physics_ensemble[member] = initialize_thermal_physics( fe_degree, quadrature_type, local_communicator, database_ensemble[member], *geometry_ensemble[member], *boundary_ensemble[member], *material_properties_ensemble[member]); heat_sources_ensemble[member] = thermal_physics_ensemble[member]->get_heat_sources(); if (restart == false) { thermal_physics_ensemble[member]->setup(); thermal_physics_ensemble[member]->initialize_dof_vector( database_ensemble[member].get("materials.initial_temperature", 300.), solution_augmented_ensemble[member].block(base_state)); } else { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("restart from file"); #endif thermal_physics_ensemble[member]->load_checkpoint( restart_filename + '_' + std::to_string(member), solution_augmented_ensemble[member].block(base_state)); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("restart from file"); #endif } solution_augmented_ensemble[member].collect_sizes(); // For now we only output temperature post_processor_database.put("thermal_output", true); post_processor_ensemble.push_back( std::make_unique>( local_communicator, post_processor_database, thermal_physics_ensemble[member]->get_dof_handler(), first_local_member + member)); } // PostProcessor for outputting the experimental data boost::property_tree::ptree post_processor_expt_database; // PropertyTreeInput post_processor.filename_prefix std::string expt_file_prefix = post_processor_database.get("filename_prefix") + ".expt"; post_processor_expt_database.put("filename_prefix", expt_file_prefix); post_processor_expt_database.put("thermal_output", true); std::unique_ptr> post_processor_expt; // PropertyTreeInput experiment.output_experiment_on_mesh bool const output_experiment_on_mesh = experiment_optional_database ? experiment_optional_database->get("output_experiment_on_mesh", true) : false; // Optionally output the experimental data projected onto the mesh. Since the // experimental data is unique we may not need all the processors to write the // output files. if (output_experiment_on_mesh && (my_color == 0)) { post_processor_expt = std::make_unique>( local_communicator, post_processor_expt_database, thermal_physics_ensemble[0]->get_dof_handler()); } // ----- Initialize time and time stepping counters ----- unsigned int progress = 0; unsigned int n_time_step = 0; double time = 0.; double activation_time_end = -1.; double da_time = -1.; if (restart == true) { if (global_rank == 0) { std::cout << "Restarting from file" << std::endl; } std::ifstream file{restart_filename + "_time.txt"}; boost::archive::text_iarchive ia{file}; ia >> time; ia >> n_time_step; } // PropertyTreeInput geometry.deposition_time double const activation_time = geometry_database.get("deposition_time", 0.); // ----- Read the experimental data ----- std::vector> frame_time_stamps; std::unique_ptr> experimental_data; unsigned int experimental_frame_index = std::numeric_limits::max(); if (experiment_optional_database) { auto experiment_database = experiment_optional_database.get(); // PropertyTreeInput experiment.read_in_experimental_data bool const read_in_experimental_data = experiment_database.get("read_in_experimental_data", false); if (read_in_experimental_data) { if (global_rank == 0) std::cout << "Reading the experimental log file..." << std::endl; frame_time_stamps = adamantine::read_frame_timestamps(experiment_database); adamantine::ASSERT_THROW(frame_time_stamps.size() > 0, "Experimental data parsing is activated, but " "the log shows zero cameras."); adamantine::ASSERT_THROW(frame_time_stamps[0].size() > 0, "Experimental data parsing is activated, but " "the log shows zero data frames."); if (global_rank == 0) std::cout << "Done. Log entries found for " << frame_time_stamps.size() << " camera(s), with " << frame_time_stamps[0].size() << " frame(s)." << std::endl; // PropertyTreeInput experiment.format std::string experiment_format = experiment_database.get("format"); if (boost::iequals(experiment_format, "point_cloud")) { experimental_data = std::make_unique>( adamantine::PointCloud(experiment_database)); } else { if constexpr (dim == 3) { experimental_data = std::make_unique(adamantine::RayTracing( experiment_database, thermal_physics_ensemble[0]->get_dof_handler())); } } // Advance the experimental frame counter upon restart, if necessary if (restart) { bool found_frame = false; while (!found_frame) { if (frame_time_stamps[0][experimental_frame_index + 1] < time) { experimental_frame_index++; } else { found_frame = true; } } } } } // ----- Output the initial solution ----- std::unique_ptr> mechanical_physics; dealii::LA::distributed::Vector displacement; for (unsigned int member = 0; member < local_ensemble_size; ++member) { output_pvtu(*post_processor_ensemble[member], n_time_step, time, thermal_physics_ensemble[member], solution_augmented_ensemble[member].block(base_state), mechanical_physics, displacement, *material_properties_ensemble[member], timers); } // ----- Increment the time step ----- ++n_time_step; // ----- Get refinement and time stepping parameters ----- // PropertyTreeInput refinement.time_steps_between_refinement unsigned int const time_steps_refinement = refinement_database.get("time_steps_between_refinement", 10); // PropertyTreeInput time_stepping.time_step double time_step = time_stepping_database.get("time_step"); double const da_time_half_window = 1.01 * time_step; // PropertyTreeInput time_stepping.scan_path_for_duration bool const scan_path_for_duration = time_stepping_database.get("scan_path_for_duration", false); // PropertyTreeInput time_stepping.duration double duration = scan_path_for_duration ? std::numeric_limits::max() : time_stepping_database.get("duration"); // PropertyTreeInput post_processor.time_steps_between_output unsigned int const time_steps_output = post_processor_database.get("time_steps_between_output", 1); // PropertyTreeInput post_processor.output_on_data_assimilation bool const output_on_da = post_processor_database.get("output_on_data_assimilation", true); // ----- Deposit material ----- // For now assume that all ensemble members share the same geometry (they // have independent adamantine::Geometry objects, but all are constructed // from identical parameters), base new additions on the 0th ensemble member // since all the sources use the same scan path. auto [material_deposition_boxes, deposition_times, deposition_cos, deposition_sin] = adamantine::create_material_deposition_boxes( geometry_database, heat_sources_ensemble[0]); // ----- Compute bounding heat sources ----- // When using AMR, we refine the cells that the heat sources intersect. Since // each ensemble members can have slightly different sources, we create a new // set of heat sources that encompasses the member sources. We use these new // sources when refining the mesh. auto bounding_heat_sources = adamantine::get_bounding_heat_sources( database_ensemble, global_communicator); // ----- Main time stepping loop ----- if (global_rank == 0) std::cout << "Starting the main time stepping loop..." << std::endl; // If the duration is derived from the scan path, we get the time of the last // segment. If the scan path file contains SCAN_PATH_END, we set the final // duration. double current_scan_path_end = 0; if (scan_path_for_duration) { bool scan_path_end = true; double current_end_time = std::numeric_limits::max(); // We assume that all the scan paths are identical for all the ensemble // members. for (auto &source : heat_sources_ensemble[0]) { double scan_path_end_time = source->get_scan_path().get_segment_list().back().end_time; if (current_end_time > scan_path_end_time) current_end_time = scan_path_end_time; if (!source->get_scan_path().is_finished()) scan_path_end = false; } current_scan_path_end = current_end_time; if (scan_path_end) duration = current_scan_path_end; } #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_BEGIN(main_loop_id, "main_loop"); #endif while (time < duration) { #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_ITERATION(main_loop_id, n_time_step - 1); #endif // If we use scan_path_for_duration, we may need to read the scan path // file once again. // We use an epsilon to get the "expected" behavior when the deposition // time and the time match should match exactly but don't because of // floating point accuracy. double const time_step_tol = time_step * 1e-6; // The condition is written in such a way that it avoids the subtraction of // a large number (current_scan_path_end) with a much smaller one // (time_step_tol). if (scan_path_for_duration && (time - current_scan_path_end > -time_step_tol)) { // Check if we have reached the end of the file. If not, read the // updated scan path file. We assume that the scan paths are identical // for all the heat sources and thus, we can use // heat_sources_ensemble[0] to get the material deposition boxes and // times. We still need every ensemble member to read the scan path in // order to compute the correct heat sources. bool scan_path_end = true; double current_end_time = std::numeric_limits::max(); for (unsigned int member = 0; member < local_ensemble_size; ++member) { for (auto &source : heat_sources_ensemble[member]) { if (!source->get_scan_path().is_finished()) { scan_path_end = false; // This functions waits for the scan path file to be updated // before reading the file. source->get_scan_path().read_file(); // Update the current_end_time double scan_path_end_time = source->get_scan_path().get_segment_list().back().end_time; if (current_end_time > scan_path_end_time) current_end_time = scan_path_end_time; } } } current_scan_path_end = current_end_time; if (scan_path_end && (current_scan_path_end != std::numeric_limits::max())) duration = current_scan_path_end; std::tie(material_deposition_boxes, deposition_times, deposition_cos, deposition_sin) = adamantine::create_material_deposition_boxes( geometry_database, heat_sources_ensemble[0]); } if ((time + time_step) > duration) time_step = duration - time; // ----- Refine the mesh if necessary ----- // Refine the mesh the first time we get in the loop and after // time_steps_refinement time steps. if ((n_time_step == 1) || ((n_time_step % time_steps_refinement) == 0)) { timers[adamantine::refine].start(); double const next_refinement_time = time + time_steps_refinement * time_step; for (unsigned int member = 0; member < local_ensemble_size; ++member) { // FIXME std::unique_ptr> dummy; refine_mesh(thermal_physics_ensemble[member], dummy, *material_properties_ensemble[member], solution_augmented_ensemble[member].block(base_state), bounding_heat_sources, time, next_refinement_time, time_steps_refinement, refinement_database); solution_augmented_ensemble[member].collect_sizes(); } timers[adamantine::refine].stop(); if ((global_rank == 0) && (verbose_output == true)) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (thermal) after mesh refinement: " << thermal_physics_ensemble[0]->get_dof_handler().n_dofs() << std::endl; } } // We use an epsilon to get the "expected" behavior when the deposition // time and the time match should match exactly but don't because of // floating point accuracy. timers[adamantine::add_material_activate].start(); if (time - activation_time_end > -time_step_tol) { auto activation_start = std::lower_bound(deposition_times.begin(), deposition_times.end(), time - time_step_tol) - deposition_times.begin(); activation_time_end = std::min(time + std::max(activation_time, time_step), duration); auto activation_end = std::lower_bound(deposition_times.begin(), deposition_times.end(), activation_time_end) - deposition_times.begin(); if (activation_start < activation_end) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("add material"); #endif for (unsigned int member = 0; member < local_ensemble_size; ++member) { // Compute the elements to activate. // TODO Right now, we compute the list of cells that get activated // for the entire material deposition. We should restrict the list // to the cells that are activated between activation_start and // activation_end. timers[adamantine::add_material_search].start(); auto elements_to_activate = adamantine::get_elements_to_activate( *geometry_ensemble[member], thermal_physics_ensemble[member]->get_dof_handler(), material_deposition_boxes); timers[adamantine::add_material_search].stop(); // For now assume that all deposited material has never been // melted (may or may not be reasonable) std::vector has_melted(deposition_cos.size(), false); thermal_physics_ensemble[member]->add_material_start( elements_to_activate, deposition_cos, deposition_sin, has_melted, activation_start, activation_end, solution_augmented_ensemble[member].block(base_state)); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("refine triangulation"); #endif dealii::DoFHandler &dof_handler = thermal_physics_ensemble[member]->get_dof_handler(); dealii::parallel::distributed::Triangulation &triangulation = dynamic_cast &>( const_cast &>( dof_handler.get_triangulation())); triangulation.execute_coarsening_and_refinement(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("refine triangulation"); #endif thermal_physics_ensemble[member]->add_material_end( database_ensemble[member].get( "materials.new_material_temperature", 300.), solution_augmented_ensemble[member].block(base_state)); solution_augmented_ensemble[member].collect_sizes(); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("add material"); #endif } if ((global_rank == 0) && (verbose_output == true) && (activation_end - activation_start > 0)) { std::cout << "n_time_step: " << n_time_step << " time: " << time << " n_dofs (thermal) after cell activation: " << thermal_physics_ensemble[0]->get_dof_handler().n_dofs() << std::endl; } } timers[adamantine::add_material_activate].stop(); // ----- Evolve the solution by one time step ----- double const old_time = time; timers[adamantine::evol_time].start(); for (unsigned int member = 0; member < local_ensemble_size; ++member) { time = thermal_physics_ensemble[member]->evolve_one_time_step( old_time, time_step, solution_augmented_ensemble[member].block(base_state), timers); } timers[adamantine::evol_time].stop(); // ----- Perform data assimilation ----- if (assimilate_data) { for (unsigned int member = 0; member < local_ensemble_size; ++member) { thermal_physics_ensemble[member]->get_affine_constraints().distribute( solution_augmented_ensemble[member].block(base_state)); } // Currently assume that all frames are synced so that the 0th camera // frame time is the relevant time double frame_time = std::numeric_limits::max(); if ((experimental_frame_index + 1) < frame_time_stamps[0].size()) { if (time > da_time + da_time_half_window) { da_time = frame_time_stamps[0][experimental_frame_index + 1]; } if (frame_time_stamps[0][experimental_frame_index + 1] <= time) { experimental_frame_index = experimental_data->read_next_frame(); frame_time = frame_time_stamps[0][experimental_frame_index]; } } if (frame_time <= time) { adamantine::ASSERT_THROW( frame_time > old_time || n_time_step == 1, "Unexpectedly missed a data assimilation frame."); if (global_rank == 0) std::cout << "Performing data assimilation at time " << time << "..." << std::endl; // Print out the augmented parameters for (unsigned int member = 0; member < local_ensemble_size; ++member) { std::cout << "Rank: " << global_rank << " | Old parameters for member " << first_local_member + member << ": "; for (auto param : solution_augmented_ensemble[member].block(1)) std::cout << param << " "; std::cout << std::endl; } timers[adamantine::da_experimental_data].start(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("da_experimental_data"); #endif auto points_values = experimental_data->get_points_values(); auto const &thermal_dof_handler = thermal_physics_ensemble[0]->get_dof_handler(); auto expt_to_dof_mapping = adamantine::get_expt_to_dof_mapping( points_values, thermal_dof_handler); std::cout << "Rank: " << global_rank << " | Number expt sites mapped to DOFs: " << expt_to_dof_mapping.first.size() << std::endl; #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("da_experimental_data"); #endif timers[adamantine::da_experimental_data].stop(); if (post_processor_expt) { dealii::LA::distributed::Vector temperature_expt; temperature_expt.reinit( solution_augmented_ensemble[0].block(base_state)); temperature_expt.add(1.0e10); adamantine::set_with_experimental_data( global_communicator, points_values, expt_to_dof_mapping, temperature_expt, verbose_output); thermal_physics_ensemble[0]->get_affine_constraints().distribute( temperature_expt); post_processor_expt ->template write_thermal_output::array_layout>( n_time_step, time, temperature_expt, material_properties_ensemble[0]->get_state(), material_properties_ensemble[0]->get_dofs_map(), material_properties_ensemble[0]->get_dof_handler()); } // Only continue data assimilation if some of the observations are // mapped to DOFs if (expt_to_dof_mapping.first.size() > 0) { // NOTE: As is, this updates the dof mapping and covariance sparsity // pattern for every data assimilation operation. Strictly, this is // only necessary if the mesh changes (both updates) or the locations // of observations changes (the dof mapping). In practice, changes to // the mesh due to deposition likely cause the updates to be required // for each operation. If this is a bottleneck, it can be fixed in the // future. timers[adamantine::da_dof_mapping].start(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("da_dof_mapping"); #endif data_assimilator.update_dof_mapping(expt_to_dof_mapping); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("da_dof_mapping"); #endif timers[adamantine::da_dof_mapping].stop(); timers[adamantine::da_covariance_sparsity].start(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("da_covariance_sparsity"); #endif data_assimilator.update_covariance_sparsity_pattern( thermal_dof_handler, solution_augmented_ensemble[0].block(augmented_state).size()); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("da_covariance_sparsity"); #endif timers[adamantine::da_covariance_sparsity].start(); unsigned int experimental_data_size = points_values.values.size(); // Create the R matrix (the observation covariance matrix) // PropertyTreeInput experiment.estimated_uncertainty timers[adamantine::da_obs_covariance].start(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("da_obs_covariance"); #endif double variance_entries = experiment_optional_database.get().get( "estimated_uncertainty", 0.0); variance_entries = variance_entries * variance_entries; dealii::SparsityPattern pattern(experimental_data_size, experimental_data_size, 1); for (unsigned int i = 0; i < experimental_data_size; ++i) { pattern.add(i, i); } pattern.compress(); dealii::SparseMatrix R(pattern); for (unsigned int i = 0; i < experimental_data_size; ++i) { R.add(i, i, variance_entries); } #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("da_obs_covariance"); #endif timers[adamantine::da_obs_covariance].stop(); // Perform data assimilation to update the augmented state ensemble timers[adamantine::da_update_ensemble].start(); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("da_update_ensemble"); #endif data_assimilator.update_ensemble(solution_augmented_ensemble, points_values.values, R); #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("da_update_ensemble"); #endif timers[adamantine::da_update_ensemble].stop(); // Extract the parameters from the augmented state for (unsigned int member = 0; member < local_ensemble_size; ++member) { for (unsigned int index = 0; index < augmented_state_parameters.size(); ++index) { // FIXME: Need to consider how we want to generalize this. It // could get unwieldy if we want to specify every parameter of an // arbitrary number of beams. if (augmented_state_parameters.at(index) == adamantine::AugmentedStateParameters::beam_0_absorption) { database_ensemble[member].put( "sources.beam_0.absorption_efficiency", solution_augmented_ensemble[member].block( augmented_state)[index]); } else if (augmented_state_parameters.at(index) == adamantine::AugmentedStateParameters::beam_0_max_power) { database_ensemble[member].put( "sources.beam_0.max_power", solution_augmented_ensemble[member].block( augmented_state)[index]); } } } if (global_rank == 0) std::cout << "Done." << std::endl; // Print out the augmented parameters if (solution_augmented_ensemble[0].block(1).size() > 0) { for (unsigned int member = 0; member < local_ensemble_size; ++member) { std::cout << "Rank: " << global_rank << " | New parameters for member " << first_local_member + member << ": "; for (auto param : solution_augmented_ensemble[member].block(1)) std::cout << param << " "; std::cout << std::endl; } } } else { if (global_rank == 0) std::cout << "WARNING: NO EXPERIMENTAL DATA POINTS MAPPED ONTO THE " "SIMULATION MESH. SKIPPING DATA ASSIMILATION OPERATION." << std::endl; } } // Update the heat source in the ThermalPhysics objects for (unsigned int member = 0; member < local_ensemble_size; ++member) { thermal_physics_ensemble[member]->update_physics_parameters( database_ensemble[member].get_child("sources")); } } // ----- Checkpoint the ensemble members ----- if (n_time_step % time_steps_checkpoint == 0) { #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_BEGIN("save checkpoint"); #endif if (global_rank == 0) { std::cout << "Checkpoint reached" << std::endl; } std::string output_dir = post_processor_database.get("output_dir", ""); std::string filename_prefix = checkpoint_overwrite ? checkpoint_filename : checkpoint_filename + '_' + std::to_string(n_time_step); for (unsigned int member = 0; member < local_ensemble_size; ++member) { thermal_physics_ensemble[member]->save_checkpoint( filename_prefix + '_' + std::to_string(first_local_member + member), solution_augmented_ensemble[member].block(base_state)); } std::ofstream file{output_dir + filename_prefix + "_time.txt"}; boost::archive::text_oarchive oa{file}; oa << time; oa << n_time_step; #ifdef ADAMANTINE_WITH_CALIPER CALI_MARK_END("save checkpoint"); #endif } // ----- Output progress on screen ----- if (global_rank == 0) { double adim_time = time / (duration / 10.); double int_part = 0; std::modf(adim_time, &int_part); if (int_part > progress) { std::cout << int_part * 10 << '%' << " completed" << std::endl; ++progress; } } // ----- Output the solution ----- if ((n_time_step % time_steps_output == 0) || (output_on_da && time > (da_time - da_time_half_window) && time < (da_time + da_time_half_window))) { for (unsigned int member = 0; member < local_ensemble_size; ++member) { thermal_physics_ensemble[member]->set_state_to_material_properties(); output_pvtu(*post_processor_ensemble[member], n_time_step, time, thermal_physics_ensemble[member], solution_augmented_ensemble[member].block(base_state), mechanical_physics, displacement, *material_properties_ensemble[member], timers); } } ++n_time_step; } #ifdef ADAMANTINE_WITH_CALIPER CALI_CXX_MARK_LOOP_END(main_loop_id); #endif for (unsigned int member = 0; member < local_ensemble_size; ++member) { post_processor_ensemble[member]->write_pvd(); } // This is only used for integration test if constexpr (std::is_same_v) { for (unsigned int member = 0; member < local_ensemble_size; ++member) { thermal_physics_ensemble[member]->get_affine_constraints().distribute( solution_augmented_ensemble[member].block(base_state)); } return solution_augmented_ensemble; } else { // NOTE: Currently unused. Added for the future case where run_ensemble is // functional on the device. std::vector> solution_augmented_ensemble_host(local_ensemble_size); for (unsigned int member = 0; member < local_ensemble_size; ++member) { solution_augmented_ensemble[member].reinit(2); solution_augmented_ensemble_host[member].block(0).reinit( solution_augmented_ensemble[member].block(0).get_partitioner()); solution_augmented_ensemble_host[member].block(0).import_elements( solution_augmented_ensemble[member].block(0), dealii::VectorOperation::insert); thermal_physics_ensemble[member]->get_affine_constraints().distribute( solution_augmented_ensemble_host[member].block(0)); solution_augmented_ensemble_host[member].block(1).reinit( solution_augmented_ensemble[member].block(0).get_partitioner()); solution_augmented_ensemble_host[member].block(1).import_elements( solution_augmented_ensemble[member].block(1), dealii::VectorOperation::insert); } return solution_augmented_ensemble_host; } } #endif