diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f78aa70141..1f87d42ab9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -23,6 +23,12 @@ install( # Add the conduction examples add_subdirectory(conduction) +# Add the solid mechanics examples +add_subdirectory(solid_mechanics) + +# Add the thermo-mechanics examples +add_subdirectory(thermo_mechanics) + # Add the contact examples add_subdirectory(contact) diff --git a/examples/contact/homotopy/two_blocks.cpp b/examples/contact/homotopy/two_blocks.cpp index a2c55a7bb0..28e8d7f1e8 100644 --- a/examples/contact/homotopy/two_blocks.cpp +++ b/examples/contact/homotopy/two_blocks.cpp @@ -30,9 +30,9 @@ using DensitySpace = smith::L2; using SolidMaterial = smith::solid_mechanics::NeoHookeanWithFieldDensity; using SolidWeakFormT = - smith::TimeDiscretizedWeakForm, - smith::Parameters, smith::H1, - smith::H1, DensitySpace>>; + smith::FunctionalWeakForm, + smith::Parameters, smith::H1, + smith::H1, DensitySpace>>; enum FIELD { diff --git a/examples/inertia_relief/inertia_relief_example.cpp b/examples/inertia_relief/inertia_relief_example.cpp index 90cac8f221..dbfc4527f0 100644 --- a/examples/inertia_relief/inertia_relief_example.cpp +++ b/examples/inertia_relief/inertia_relief_example.cpp @@ -38,9 +38,9 @@ using DensitySpace = smith::L2; using SolidMaterial = smith::solid_mechanics::NeoHookeanWithFieldDensity; using SolidWeakFormT = - smith::TimeDiscretizedWeakForm, - smith::Parameters, smith::H1, - smith::H1, DensitySpace>>; + smith::FunctionalWeakForm, + smith::Parameters, smith::H1, + smith::H1, DensitySpace>>; enum FIELD { @@ -212,20 +212,17 @@ int main(int argc, char* argv[]) ObjectiveT mass_objective("mass constraining", mesh, param_space_ptrs); - mass_objective.addBodyIntegral(smith::DependsOn<1>{}, mesh->entireBodyName(), - [](double /*t*/, auto /*X*/, auto RHO) { return get(RHO); }); + mass_objective.addBodyIntegral( + mesh->entireBodyName(), [](auto /*t_info*/, auto /*X*/, auto /*U*/, auto RHO) { return get(RHO); }); double mass = mass_objective.evaluate(time_info, shape_disp.get(), objective_states); smith::tensor initial_cg; // center of gravity for (int i = 0; i < dim; ++i) { auto cg_objective = std::make_shared("translation " + std::to_string(i), mesh, param_space_ptrs); - cg_objective->addBodyIntegral(smith::DependsOn<0, 1>{}, mesh->entireBodyName(), - [i](double - /*time*/, - auto X, auto U, auto RHO) { - return (get(X)[i] + get(U)[i]) * get(RHO); - }); + cg_objective->addBodyIntegral(mesh->entireBodyName(), [i](auto /*t_info*/, auto X, auto U, auto RHO) { + return (get(X)[i] + get(U)[i]) * get(RHO); + }); initial_cg[i] = cg_objective->evaluate(time_info, shape_disp.get(), objective_states) / mass; constraints.push_back(cg_objective); @@ -234,8 +231,8 @@ int main(int argc, char* argv[]) for (int i = 0; i < dim; ++i) { auto center_rotation_objective = std::make_shared("rotation" + std::to_string(i), mesh, param_space_ptrs); - center_rotation_objective->addBodyIntegral(smith::DependsOn<0, 1>{}, mesh->entireBodyName(), - [i, initial_cg](double /*time*/, auto X, auto U, auto RHO) { + center_rotation_objective->addBodyIntegral(mesh->entireBodyName(), + [i, initial_cg](auto /*t_info*/, auto X, auto U, auto RHO) { auto u = get(U); auto x = get(X) + u; auto dx = x - initial_cg; diff --git a/examples/solid_mechanics/CMakeLists.txt b/examples/solid_mechanics/CMakeLists.txt new file mode 100644 index 0000000000..16040fb520 --- /dev/null +++ b/examples/solid_mechanics/CMakeLists.txt @@ -0,0 +1,24 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and +# other Smith Project Developers. See the top-level LICENSE file for +# details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +smith_add_executable( NAME composable_solid_mechanics + SOURCES composable_solid_mechanics.cpp + OUTPUT_DIR ${EXAMPLE_OUTPUT_DIRECTORY} + DEPENDS_ON smith + ) + +install( + FILES + composable_solid_mechanics.cpp + DESTINATION + examples/smith/solid_mechanics +) + +if(SMITH_ENABLE_TESTS) + blt_add_test(NAME composable_solid_mechanics + COMMAND composable_solid_mechanics + NUM_MPI_TASKS 1 ) +endif() diff --git a/examples/solid_mechanics/composable_solid_mechanics.cpp b/examples/solid_mechanics/composable_solid_mechanics.cpp new file mode 100644 index 0000000000..60d415da28 --- /dev/null +++ b/examples/solid_mechanics/composable_solid_mechanics.cpp @@ -0,0 +1,211 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file composable_solid_mechanics.cpp + * @brief Dynamic solid-mechanics example using composable differentiable numerics systems. + */ + +#include +#include +#include +#include + +// _includes_start +#include "smith/infrastructure/application_manager.hpp" +#include "smith/physics/state/state_manager.hpp" +#include "smith/physics/mesh.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/physics/functional_objective.hpp" +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/differentiable_physics.hpp" +#include "smith/differentiable_numerics/evaluate_objective.hpp" +#include "smith/differentiable_numerics/differentiable_test_utils.hpp" +#include "smith/differentiable_numerics/paraview_writer.hpp" +#include "smith/physics/materials/solid_material.hpp" +// _includes_end + +namespace { + +struct YoungsModulusNeoHookeanWithTimeInfo { + using State = smith::solid_mechanics::NeoHookean::State; + + double density; + double nu; + + template + auto operator()(const smith::TimeInfo&, [[maybe_unused]] State& state, const smith::tensor& grad_u, + const GradVType&, const YoungsType& youngs_modulus) const + { + using std::log1p; + constexpr auto I = smith::Identity(); + auto E = smith::get<0>(youngs_modulus); + auto G = E / (2.0 * (1.0 + nu)); + auto K = E / (3.0 * (1.0 - 2.0 * nu)); + auto lambda = K - (2.0 / dim) * G; + auto B_minus_I = grad_u * transpose(grad_u) + transpose(grad_u) + grad_u; + auto logJ = log1p(detApIm1(grad_u)); + auto TK = lambda * logJ * I + G * B_minus_I; + auto F = grad_u + I; + return dot(TK, inv(transpose(F))); + } +}; + +} // namespace + +int main(int argc, char* argv[]) +{ + // _init_start + smith::ApplicationManager application_manager(argc, argv); + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "composable_solid_mechanics"); + // _init_end + + // _mesh_start + constexpr int dim = 3; + constexpr int order = 1; + + // Build a slender beam mesh and name the loaded and fixed boundaries. + auto mesh = std::make_shared( + mfem::Mesh::MakeCartesian3D(8, 2, 2, mfem::Element::HEXAHEDRON, 1.0, 0.1, 0.1), "mesh", 0, 0); + mesh->addDomainOfBoundaryElements("left", smith::by_attr(3)); + mesh->addDomainOfBoundaryElements("right", smith::by_attr(5)); + // _mesh_end + + // _solver_start + smith::LinearSolverOptions linear_options{.linear_solver = smith::LinearSolver::CG, + .preconditioner = smith::Preconditioner::HypreAMG, + .relative_tol = 1e-6, + .absolute_tol = 1e-10, + .max_iterations = 80, + .print_level = 0}; + smith::NonlinearSolverOptions nonlinear_options{.nonlin_solver = smith::NonlinearSolver::TrustRegion, + .relative_tol = 1e-7, + .absolute_tol = 1e-8, + .max_iterations = 15, + .print_level = 0}; + + smith::SolidMechanicsOptions output_options{.enable_stress_output = true, .output_cauchy_stress = true}; + // _solver_end + + // _build_start + // Build a dynamic solid system with Young's modulus as a differentiable field parameter. + auto solid_system = smith::buildSolidMechanicsSystem( + nonlinear_options, linear_options, output_options, mesh, smith::FieldType>("youngs_modulus")); + + constexpr double E = 100.0; + constexpr double nu = 0.25; + solid_system->setMaterial(YoungsModulusNeoHookeanWithTimeInfo{.density = 1.0, .nu = nu}, mesh->entireBodyName()); + // _build_end + + // _bc_start + // Clamp selected components on the left and load the beam through body force and traction terms. + solid_system->setDisplacementBC(mesh->domain("left"), std::vector{0, 2}); + solid_system->addBodyForce(mesh->entireBodyName(), [](double, auto X, auto, auto, auto, auto... /*args*/) { + auto body_force = 0.0 * X; + body_force[1] = -0.02; + return body_force; + }); + solid_system->addTraction("right", [](double, auto X, auto, auto, auto, auto, auto... /*args*/) { + auto traction = 0.0 * X; + traction[0] = -0.01; + return traction; + }); + // _bc_end + + // _ic_start + auto physics = smith::makeDifferentiablePhysics(solid_system, "composable_solid_mechanics"); + + if (solid_system->cycle_zero_systems.empty()) { + throw std::runtime_error("Expected cycle-zero solve for implicit dynamics."); + } + + physics->getFieldParam("param_youngs_modulus").get()->setFromFieldFunction([=](smith::tensor) { + return E; + }); + + auto initial_displacement = [](smith::tensor X) { + auto displacement = 0.0 * X; + displacement[0] = 1.0e-3 * X[0]; + return displacement; + }; + auto initial_velocity = [](smith::tensor X) { + auto velocity = 0.0 * X; + velocity[1] = 2.0e-2 * X[0]; + return velocity; + }; + + // Seed all time-integration states before the initial acceleration solve. + physics->getInitialFieldState("displacement_solve_state").get()->setFromFieldFunction(initial_displacement); + physics->getInitialFieldState("displacement").get()->setFromFieldFunction(initial_displacement); + physics->getInitialFieldState("velocity").get()->setFromFieldFunction(initial_velocity); + physics->getInitialFieldState("acceleration").get()->setFromFieldFunction([](smith::tensor) { + return smith::tensor{}; + }); + // _ic_end + + // _run_start + using DispSpace = smith::H1; + // Accumulate a scalar QoI over the trajectory for reverse-mode sensitivity checks. + const auto qoi_fields = std::vector{physics->getInitialFieldState("displacement"), + physics->getInitialFieldState("velocity")}; + smith::FunctionalObjective> qoi("solid_dynamic_energy_proxy", mesh, + smith::spaces(qoi_fields)); + qoi.addBodyIntegral(mesh->entireBodyName(), [](const smith::TimeInfo&, auto, auto U, auto V) { + auto u = smith::get(U); + auto v = smith::get(V); + return 0.5 * (u[0] * u[0] + u[1] * u[1] + u[2] * u[2]) + 0.05 * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + }); + + constexpr double dt = 0.25; + constexpr int num_steps = 3; + auto current_qoi_fields = [&]() { + return std::vector{physics->getFieldState("displacement"), physics->getFieldState("velocity")}; + }; + auto qoi_state = + 0.0 * smith::evaluateObjective(qoi, physics->getShapeDispFieldState(), qoi_fields, + smith::TimeInfo(physics->time(), dt, static_cast(physics->cycle()))); + for (int step = 0; step < num_steps; ++step) { + physics->advanceTimestep(dt); + qoi_state = qoi_state + + smith::evaluateObjective(qoi, physics->getShapeDispFieldState(), current_qoi_fields(), + smith::TimeInfo(physics->time(), dt, static_cast(physics->cycle()))); + } + + SLIC_INFO_ROOT(std::format("reaction norm: {}", physics->getReactionStates().front().get()->Norml2())); + gretl::set_as_objective(qoi_state); + SLIC_INFO_ROOT(std::format("QoI value: {}", qoi_state.get())); + qoi_state.data_store().back_prop(); + auto shape_displacement = physics->getShapeDispFieldState(); + auto initial_displacement_state = physics->getInitialFieldState("displacement"); + auto initial_velocity_state = physics->getInitialFieldState("velocity"); + auto youngs_modulus_state = physics->getFieldParam("param_youngs_modulus"); + SLIC_INFO_ROOT(std::format("dQoI/d(shape) norm: {}", shape_displacement.get_dual()->Norml2())); + SLIC_INFO_ROOT(std::format("dQoI/d(youngs_modulus) norm: {}", youngs_modulus_state.get_dual()->Norml2())); + SLIC_INFO_ROOT(std::format("dQoI/d(initial displacement) norm: {}", initial_displacement_state.get_dual()->Norml2())); + SLIC_INFO_ROOT(std::format("dQoI/d(initial velocity) norm: {}", initial_velocity_state.get_dual()->Norml2())); + SLIC_INFO_ROOT( + std::format("shape FD rate: {}", smith::checkGradWrt(qoi_state, shape_displacement, 1.0e-2, 4, false))); + SLIC_INFO_ROOT(std::format("youngs_modulus FD rate: {}", + smith::checkGradWrt(qoi_state, youngs_modulus_state, 5.0e-2, 4, false))); + SLIC_INFO_ROOT(std::format("initial displacement FD rate: {}", + smith::checkGradWrt(qoi_state, initial_displacement_state, 5.0e-3, 4, false))); + SLIC_INFO_ROOT(std::format("initial velocity FD rate: {}", + smith::checkGradWrt(qoi_state, initial_velocity_state, 5.0e-3, 4, false))); + // _run_end + + // _output_start + auto writer = + smith::createParaviewWriter(*mesh, physics->getFieldStatesAndParamStates(), "paraview_composable_solid_mechanics", + smith::ParaviewWriter::Options{.write_duals = false}); + writer.write(physics->cycle(), physics->time(), physics->getFieldStatesAndParamStates()); + SLIC_INFO_ROOT("ParaView output: paraview_composable_solid_mechanics"); + // _output_end + + return 0; +} diff --git a/examples/thermo_mechanics/CMakeLists.txt b/examples/thermo_mechanics/CMakeLists.txt new file mode 100644 index 0000000000..fe93ead407 --- /dev/null +++ b/examples/thermo_mechanics/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and +# other Smith Project Developers. See the top-level LICENSE file for +# details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +smith_add_executable( NAME composable_thermo_mechanics + SOURCES composable_thermo_mechanics.cpp + OUTPUT_DIR ${EXAMPLE_OUTPUT_DIRECTORY} + DEPENDS_ON smith + ) + +smith_add_executable( NAME composable_thermo_mechanics_advanced + SOURCES composable_thermo_mechanics_advanced.cpp + OUTPUT_DIR ${EXAMPLE_OUTPUT_DIRECTORY} + DEPENDS_ON smith + ) + +install( + FILES + composable_thermo_mechanics.cpp + composable_thermo_mechanics_advanced.cpp + DESTINATION + examples/smith/thermo_mechanics +) + +if(SMITH_ENABLE_TESTS) + blt_add_test(NAME composable_thermo_mechanics + COMMAND composable_thermo_mechanics + NUM_MPI_TASKS 1 ) + blt_add_test(NAME composable_thermo_mechanics_advanced + COMMAND composable_thermo_mechanics_advanced + NUM_MPI_TASKS 1 ) +endif() diff --git a/examples/thermo_mechanics/composable_thermo_mechanics.cpp b/examples/thermo_mechanics/composable_thermo_mechanics.cpp new file mode 100644 index 0000000000..984803201f --- /dev/null +++ b/examples/thermo_mechanics/composable_thermo_mechanics.cpp @@ -0,0 +1,114 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file composable_thermo_mechanics.cpp + * @brief Minimal composable thermo-mechanics example using differentiable numerics. + */ + +#include + +// _includes_start +#include "smith/infrastructure/application_manager.hpp" +#include "smith/physics/state/state_manager.hpp" +#include "smith/physics/mesh.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_system.hpp" +#include "smith/differentiable_numerics/make_time_info_material.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" +#include "smith/differentiable_numerics/differentiable_physics.hpp" +#include "smith/physics/materials/green_saint_venant_thermoelastic.hpp" +// _includes_end + +int main(int argc, char* argv[]) +{ + // _init_start + smith::ApplicationManager application_manager(argc, argv); + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "composable_thermo_mechanics"); + // _init_end + + // _mesh_start + constexpr int dim = 3; + constexpr int order = 1; + + // Build a small beam mesh and name boundaries used by both physics systems. + auto mesh = std::make_shared( + mfem::Mesh::MakeCartesian3D(8, 2, 2, mfem::Element::HEXAHEDRON, 1.0, 0.1, 0.1), "mesh", 0, 0); + mesh->addDomainOfBoundaryElements("left", smith::by_attr(3)); + mesh->addDomainOfBoundaryElements("right", smith::by_attr(5)); + // _mesh_end + + // _solver_start + smith::LinearSolverOptions linear_options{.linear_solver = smith::LinearSolver::SuperLU, + .relative_tol = 1e-8, + .absolute_tol = 1e-10, + .max_iterations = 200, + .print_level = 0}; + smith::NonlinearSolverOptions nonlinear_options{.nonlin_solver = smith::NonlinearSolver::NewtonLineSearch, + .relative_tol = 1e-7, + .absolute_tol = 1e-8, + .max_iterations = 20, + .max_line_search_iterations = 6, + .print_level = 0}; + + auto field_store = std::make_shared(mesh, 100); + + // Register displacement and temperature states in one shared field store. + auto solid_fields = + smith::registerSolidMechanicsFields(field_store); + auto thermal_fields = + smith::registerThermalFields(field_store); + // _solver_end + + // _build_start + auto solid_solver = + std::make_shared(smith::buildNonlinearBlockSolver(nonlinear_options, linear_options, *mesh)); + auto thermal_solver = + std::make_shared(smith::buildNonlinearBlockSolver(nonlinear_options, linear_options, *mesh)); + + auto solid_system = smith::buildSolidMechanicsSystem(solid_solver, smith::SolidMechanicsOptions{}, solid_fields, + smith::couplingFields(thermal_fields)); + auto thermal_system = smith::buildThermalSystem(thermal_solver, smith::ThermalOptions{}, thermal_fields, + smith::couplingFields(solid_fields)); + + auto coupled_system = smith::combineSystems(solid_system, thermal_system); + + // Use one thermoelastic material model in both coupled residuals. + auto material = smith::makeTimeInfoMaterial( + smith::thermomechanics::GreenSaintVenantThermoelasticMaterial{1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + smith::setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh->entireBodyName()); + // _build_end + + // _bc_start + // Clamp one end, impose a thermal gradient, then apply traction and heat source loads. + solid_system->setDisplacementBC(mesh->domain("left")); + thermal_system->setTemperatureBC(mesh->domain("left"), [](auto, auto) { return 1.0; }); + thermal_system->setTemperatureBC(mesh->domain("right"), [](auto, auto) { return 0.0; }); + + solid_system->addTraction("right", [](double, auto X, auto, auto, auto, auto, auto... /*unused*/) { + auto traction = 0.0 * X; + traction[0] = -0.01; + return traction; + }); + + thermal_system->addHeatSource(mesh->entireBodyName(), [](auto, auto, auto, auto... /*unused*/) { return 0.5; }); + // _bc_end + + // _run_start + auto physics = smith::makeDifferentiablePhysics(coupled_system, "composable_thermo_mechanics"); + // Advance the coupled system for two fixed time steps. + for (int step = 0; step < 2; ++step) { + physics->advanceTimestep(1.0); + } + // _run_end + + return 0; +} diff --git a/examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp b/examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp new file mode 100644 index 0000000000..119918de87 --- /dev/null +++ b/examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp @@ -0,0 +1,182 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file composable_thermo_mechanics_advanced.cpp + * @brief Advanced composable thermo-mechanics example with staged solves, a differentiable QoI, + * finite-difference verification, and ParaView output including Cauchy stress. + */ + +#include +#include +#include + +// _includes_start +#include "smith/infrastructure/application_manager.hpp" +#include "smith/physics/state/state_manager.hpp" +#include "smith/physics/mesh.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/physics/functional_objective.hpp" + +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_system.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" +#include "smith/differentiable_numerics/differentiable_physics.hpp" +#include "smith/differentiable_numerics/paraview_writer.hpp" +#include "smith/differentiable_numerics/evaluate_objective.hpp" +#include "smith/differentiable_numerics/differentiable_test_utils.hpp" +#include "smith/differentiable_numerics/make_time_info_material.hpp" +#include "smith/physics/materials/green_saint_venant_thermoelastic.hpp" +// _includes_end + +int main(int argc, char* argv[]) +{ + // _init_start + smith::ApplicationManager application_manager(argc, argv); + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "composable_thermo_mechanics_advanced"); + // _init_end + + // _mesh_start + constexpr int dim = 3; + constexpr int order = 1; + using DispSpace = smith::H1; + using TempSpace = smith::H1; + + // Build a small beam mesh and name boundaries used by thermal and mechanical loads. + auto mesh = std::make_shared( + mfem::Mesh::MakeCartesian3D(8, 2, 2, mfem::Element::HEXAHEDRON, 1.0, 0.1, 0.1), "mesh", 0, 0); + mesh->addDomainOfBoundaryElements("left", smith::by_attr(3)); + mesh->addDomainOfBoundaryElements("right", smith::by_attr(5)); + // _mesh_end + + // _solver_start + smith::LinearSolverOptions coupled_linear{.linear_solver = smith::LinearSolver::SuperLU, + .relative_tol = 1e-8, + .absolute_tol = 1e-10, + .max_iterations = 200, + .print_level = 0}; + smith::NonlinearSolverOptions coupled_nonlin{.nonlin_solver = smith::NonlinearSolver::NewtonLineSearch, + .relative_tol = 1e-8, + .absolute_tol = 1e-8, + .max_iterations = 12, + .max_line_search_iterations = 6, + .print_level = 0}; + + auto field_store = std::make_shared(mesh, 200); + + // Register coupled displacement, temperature, and parameter fields in one shared store. + smith::SolidMechanicsOptions solid_options{.enable_stress_output = true, .output_cauchy_stress = true}; + auto solid_fields = smith::registerSolidMechanicsFields( + field_store, solid_options); + auto thermal_fields = + smith::registerThermalFields(field_store); + auto param_fields = + smith::registerParameterFields(field_store, smith::FieldType>("thermal_expansion_scaling")); + // _solver_end + + // _build_start + auto solid_system = smith::buildSolidMechanicsSystem(nullptr, solid_options, solid_fields, + smith::couplingFields(thermal_fields), param_fields); + auto thermal_system = smith::buildThermalSystem(nullptr, smith::ThermalOptions{}, thermal_fields, + smith::couplingFields(solid_fields), param_fields); + + // Wrap the material so integrands can read TimeInfo while remaining composable. + auto material = + smith::makeTimeInfoMaterial(smith::thermomechanics::ParameterizedGreenSaintVenantThermoelasticMaterial{ + 1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + smith::setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh->entireBodyName()); + + auto coupled_solver = std::make_shared(10); + coupled_solver->addSubsystemSolver({0, 1}, smith::buildNonlinearBlockSolver(coupled_nonlin, coupled_linear, *mesh), + 1.0); + + // Combine thermal and solid residuals into a monolithic coupled solve. + auto coupled_system = smith::combineSystems(coupled_solver, solid_system, thermal_system); + std::string physics_name = "composable_thermo_mechanics_advanced"; + auto physics = smith::makeDifferentiablePhysics(coupled_system, physics_name); + auto output_states = field_store->getOutputFieldStates(); + auto output_writer = + smith::createParaviewWriter(*mesh, output_states, "paraview_composable_thermo_mechanics_advanced", + smith::ParaviewWriter::Options{.write_duals = false}); + // _build_end + + // _bc_start + field_store->getParameterFields()[0].get()->setFromFieldFunction([](smith::tensor) { return 1.0; }); + + // Clamp one end, impose a thermal gradient, then apply traction and heat source loads. + solid_system->setDisplacementBC(mesh->domain("left")); + thermal_system->setTemperatureBC(mesh->domain("left"), [](auto, auto) { return 1.0; }); + thermal_system->setTemperatureBC(mesh->domain("right"), [](auto, auto) { return 0.0; }); + + solid_system->addTraction("right", + [](double, auto X, auto /*n*/, auto /*u*/, auto /*v*/, auto /*a*/, auto... /*args*/) { + auto traction = 0.0 * X; + traction[0] = -0.005; + return traction; + }); + + thermal_system->addHeatSource(mesh->entireBodyName(), [](auto, auto... /*unused*/) { return 0.1; }); + // _bc_end + + // _qoi_start + auto fetch_qoi_fields = [&]() { + return std::vector{field_store->getField("displacement"), field_store->getField("temperature")}; + }; + // Track a differentiable energy proxy depending on displacement and temperature. + smith::FunctionalObjective> qoi("thermo_mechanical_energy_proxy", mesh, + smith::spaces(fetch_qoi_fields())); + qoi.addBodyIntegral(mesh->entireBodyName(), [](auto, auto /*X*/, auto U, auto Theta) { + auto u = smith::get(U); + auto theta = smith::get(Theta); + return 0.5 * u[0] * u[0] + 0.05 * theta * theta; + }); + + auto qoi_state = + 0.0 * smith::evaluateObjective(qoi, physics->getShapeDispFieldState(), fetch_qoi_fields(), + smith::TimeInfo(physics->time(), 1.0, static_cast(physics->cycle()))); + // _qoi_end + + // _run_start + constexpr double dt = 0.5; + constexpr int qoi_steps = 1; + for (int step = 0; step < qoi_steps; ++step) { + physics->advanceTimestep(dt); + qoi_state = qoi_state + + smith::evaluateObjective(qoi, physics->getShapeDispFieldState(), fetch_qoi_fields(), + smith::TimeInfo(physics->time(), dt, static_cast(physics->cycle()))); + } + // _run_end + + // _output_start + output_writer.write(physics->cycle(), physics->time(), field_store->getOutputFieldStates()); + + SLIC_INFO_ROOT("ParaView output: paraview_composable_thermo_mechanics_advanced"); + // _output_end + + // _sensitivity_start + // Differentiate the QoI with respect to the thermal-expansion scaling field. + gretl::set_as_objective(qoi_state); + auto qoi_value = qoi_state.get(); + SLIC_INFO_ROOT(std::format("QoI value: {}", qoi_value)); + qoi_state.data_store().back_prop(); + + auto parameter_state = field_store->getParameterFields()[0]; + auto parameter_sensitivity = parameter_state.get_dual()->Norml2(); + + SLIC_INFO_ROOT(std::format("dQoI/d(thermal_expansion_scaling) norm: {}", parameter_sensitivity)); + SLIC_ERROR_ROOT_IF(parameter_sensitivity <= 0.0, "Expected non-zero QoI sensitivity."); + + auto fd_order = smith::checkGradWrt(qoi_state, parameter_state, 5.0e-2, 4, true); + SLIC_INFO_ROOT(std::format("finite-difference convergence rate: {}", fd_order)); + SLIC_ERROR_ROOT_IF(fd_order < 0.7, "Finite-difference check did not converge."); + // _sensitivity_end + + return 0; +} diff --git a/src/docs/sphinx/user_guide/composable_solid_mechanics_tutorial.rst b/src/docs/sphinx/user_guide/composable_solid_mechanics_tutorial.rst new file mode 100644 index 0000000000..6299008ccb --- /dev/null +++ b/src/docs/sphinx/user_guide/composable_solid_mechanics_tutorial.rst @@ -0,0 +1,96 @@ +.. ## Copyright (c) Lawrence Livermore National Security, LLC and +.. ## other Smith Project Developers. See the top-level COPYRIGHT file for details. +.. ## +.. ## SPDX-License-Identifier: (BSD-3-Clause) + +############################### +Composable Solid Mechanics Demo +############################### + +This demo builds one composable solid mechanics system. It registers fields, +adds a Young's modulus parameter, applies a Neo-Hookean material, advances a +dynamic solve, checks sensitivities, and writes output. + +The full source code lives in ``examples/solid_mechanics/composable_solid_mechanics.cpp``. + +Includes and Initialization +--------------------------- + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _includes_start + :end-before: _includes_end + :language: C++ + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _init_start + :end-before: _init_end + :language: C++ + +Mesh Construction +----------------- + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _mesh_start + :end-before: _mesh_end + :language: C++ + +Solver and Field Registration +----------------------------- + +Registration declares the displacement fields and the Young's modulus parameter +on the shared ``FieldStore``. Register parameter fields with +``registerParameterFields(field_store, ...)`` before building the system. + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _solver_start + :end-before: _solver_end + :language: C++ + +System Build and Material Setup +------------------------------- + +The build step consumes the registered field pack and parameter bundle. The +material reads the parameter field and uses the ``TimeInfo`` material interface. + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _build_start + :end-before: _build_end + :language: C++ + +Boundary Conditions and Loads +----------------------------- + +The left boundary fixes selected displacement components. The body force and +traction provide the applied loads. + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _bc_start + :end-before: _bc_end + :language: C++ + +Physics Creation and Initial Conditions +--------------------------------------- + +The differentiable physics object owns the timestep loop. The example sets the +parameter field and seeds non-zero initial displacement and velocity fields. + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _ic_start + :end-before: _ic_end + :language: C++ + +Advance, Sensitivities, and Reactions +------------------------------------- + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _run_start + :end-before: _run_end + :language: C++ + +Write ParaView Output +--------------------- + +.. literalinclude:: ../../../../examples/solid_mechanics/composable_solid_mechanics.cpp + :start-after: _output_start + :end-before: _output_end + :language: C++ diff --git a/src/docs/sphinx/user_guide/composable_thermo_mechanics_advanced_tutorial.rst b/src/docs/sphinx/user_guide/composable_thermo_mechanics_advanced_tutorial.rst new file mode 100644 index 0000000000..035f9dc92f --- /dev/null +++ b/src/docs/sphinx/user_guide/composable_thermo_mechanics_advanced_tutorial.rst @@ -0,0 +1,98 @@ +.. ## Copyright (c) Lawrence Livermore National Security, LLC and +.. ## other Smith Project Developers. See the top-level COPYRIGHT file for details. +.. ## +.. ## SPDX-License-Identifier: (BSD-3-Clause) + +############################################# +Composable Thermo-Mechanics Advanced Example +############################################# + +This demo extends the minimal thermo-mechanics setup. It adds a parameter field, +a staged solver, a differentiable quantity of interest, a finite-difference +check, and ParaView output. + +The full source code lives in ``examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp``. + +Includes and Initialization +--------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _includes_start + :end-before: _includes_end + :language: C++ + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _init_start + :end-before: _init_end + :language: C++ + +Mesh and Field Setup +-------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _mesh_start + :end-before: _mesh_end + :language: C++ + +Solver Config and Field Registration +------------------------------------ + +Registration declares solid, thermal, and parameter fields on one shared +``FieldStore``. The thermal-expansion parameter is registered with +``registerParameterFields(field_store, ...)`` before either system is built. + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _solver_start + :end-before: _solver_end + :language: C++ + +System Build and Coupling +------------------------- + +The build step creates solid and thermal systems from the registered field +packs. ``combineSystems(...)`` attaches them to one staged solver. + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _build_start + :end-before: _build_end + :language: C++ + +Boundary Conditions and Loads +----------------------------- + +Boundary conditions are applied on the left and right boundaries. Loads are +added through the solid and thermal systems before timestepping. + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _bc_start + :end-before: _bc_end + :language: C++ + +QoI Definition and Timestep Advance +----------------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _qoi_start + :end-before: _qoi_end + :language: C++ + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _run_start + :end-before: _run_end + :language: C++ + +ParaView Output +--------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _output_start + :end-before: _output_end + :language: C++ + +Sensitivity and Finite-Difference Check +--------------------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics_advanced.cpp + :start-after: _sensitivity_start + :end-before: _sensitivity_end + :language: C++ diff --git a/src/docs/sphinx/user_guide/composable_thermo_mechanics_tutorial.rst b/src/docs/sphinx/user_guide/composable_thermo_mechanics_tutorial.rst new file mode 100644 index 0000000000..040d88e541 --- /dev/null +++ b/src/docs/sphinx/user_guide/composable_thermo_mechanics_tutorial.rst @@ -0,0 +1,82 @@ +.. ## Copyright (c) Lawrence Livermore National Security, LLC and +.. ## other Smith Project Developers. See the top-level COPYRIGHT file for details. +.. ## +.. ## SPDX-License-Identifier: (BSD-3-Clause) + +#################################### +Composable Thermo-Mechanics Tutorial +#################################### + +This demo builds a minimal composable thermo-mechanics problem. It registers +solid and thermal fields, builds two systems, couples them with a thermoelastic +material, and advances the combined system. + +The full source code lives in ``examples/thermo_mechanics/composable_thermo_mechanics.cpp``. + +Includes and Initialization +--------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _includes_start + :end-before: _includes_end + :language: C++ + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _init_start + :end-before: _init_end + :language: C++ + +Mesh Construction +----------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _mesh_start + :end-before: _mesh_end + :language: C++ + +Field Registration +------------------ + +Registration declares all fields on one shared ``FieldStore`` before any system +is built. Register parameters in the same phase with +``registerParameterFields(field_store, ...)`` and pass the returned bundle into +the build step. + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _solver_start + :end-before: _solver_end + :language: C++ + +System Build and Coupling +------------------------- + +The build step consumes each system's own field pack, then optional coupling and +parameter bundles. ``combineSystems(...)`` returns the combined system used by +``makeDifferentiablePhysics(...)``. + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _build_start + :end-before: _build_end + :language: C++ + +Boundary Conditions and Loads +----------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _bc_start + :end-before: _bc_end + :language: C++ + +Advance the Coupled System +-------------------------- + +.. literalinclude:: ../../../../examples/thermo_mechanics/composable_thermo_mechanics.cpp + :start-after: _run_start + :end-before: _run_end + :language: C++ + +This demo is intentionally small. Use it as a template for: + +- adding parameter fields +- enabling stress output +- adding internal variables diff --git a/src/docs/sphinx/user_guide/index.rst b/src/docs/sphinx/user_guide/index.rst index 5acf7927f3..ffd6ecb6d9 100644 --- a/src/docs/sphinx/user_guide/index.rst +++ b/src/docs/sphinx/user_guide/index.rst @@ -12,6 +12,9 @@ User Guide :maxdepth: 2 simple_conduction_tutorial + composable_solid_mechanics_tutorial + composable_thermo_mechanics_tutorial + composable_thermo_mechanics_advanced_tutorial command_line_options input_schema diff --git a/src/smith/differentiable_numerics/CMakeLists.txt b/src/smith/differentiable_numerics/CMakeLists.txt index 91dc826899..71ade68cc9 100644 --- a/src/smith/differentiable_numerics/CMakeLists.txt +++ b/src/smith/differentiable_numerics/CMakeLists.txt @@ -10,7 +10,8 @@ set(differentiable_numerics_sources differentiable_physics.cpp lumped_mass_explicit_newmark_state_advancer.cpp nonlinear_block_solver.cpp - coupled_system_solver.cpp + system_solver.cpp + system_base.cpp nonlinear_solve.cpp evaluate_objective.cpp dirichlet_boundary_conditions.cpp @@ -23,7 +24,7 @@ set(differentiable_numerics_headers state_advancer.hpp reaction.hpp nonlinear_block_solver.hpp - coupled_system_solver.hpp + system_solver.hpp differentiable_physics.hpp timestep_estimator.hpp explicit_dynamic_solve.hpp @@ -33,13 +34,17 @@ set(differentiable_numerics_headers evaluate_objective.hpp dirichlet_boundary_conditions.hpp time_integration_rule.hpp - time_discretized_weak_form.hpp paraview_writer.hpp multiphysics_time_integrator.hpp solid_mechanics_system.hpp solid_mechanics_with_internal_vars_system.hpp + state_variable_system.hpp thermal_system.hpp thermo_mechanics_system.hpp + thermo_mechanics_with_internal_vars_system.hpp + make_time_info_material.hpp + coupling_params.hpp + combined_system.hpp system_base.hpp differentiable_test_utils.hpp ) diff --git a/src/smith/differentiable_numerics/combined_system.hpp b/src/smith/differentiable_numerics/combined_system.hpp new file mode 100644 index 0000000000..fa1e1398bb --- /dev/null +++ b/src/smith/differentiable_numerics/combined_system.hpp @@ -0,0 +1,185 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file combined_system.hpp + * @brief CombinedSystem and combineSystems for composing independent physics into a coupled system. + * + * Individual physics sub-systems (SolidMechanicsSystem, ThermalSystem, ...) remain authoritative + * for their own configuration APIs. combineSystems wires them together via a shared FieldStore + * and provides a coupled setMaterial that registers integrands on both sub-system weak forms. + * + * Usage: + * @code + * auto field_store = std::make_shared(mesh, 100, "coupled"); + * auto solid_fields = registerSolidMechanicsFields(field_store); + * auto thermal_fields = registerThermalFields(field_store); + * + * auto solid_system = buildSolidMechanicsSystem( + * solid_solver, solid_opts, solid_fields, couplingFields(thermal_fields), param_fields); + * + * auto thermal_system = buildThermalSystem( + * thermal_solver, thermal_opts, thermal_fields, couplingFields(solid_fields)); + * + * auto coupled = combineSystems(solid, thermal); + * coupled->setMaterial(thermo_mech_material, domain); // tight coupling + * solid->addTraction(right, traction_fn); // loose via sub-system + * thermal->addHeatFlux(top, flux_fn); + * + * auto advancer = makeAdvancer(coupled); + * @endcode + */ + +#pragma once + +#include +#include +#include +#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/field_store.hpp" + +namespace smith { + +/** + * @brief A non-templated system wrapper that combines multiple sub-systems sharing one FieldStore. + * + * The combined solve does staggered iterations: in each sweep every sub-system is solved in + * order, writing its updated unknowns back to the shared FieldStore so subsequent sub-systems see + * the updated fields. The base class weak_forms member is the concatenation of sub-system weak + * forms, which allows makeAdvancer / MultiphysicsTimeIntegrator to integrate the combined system + * transparently. + * + * For tight coupling (setMaterial on the combined system), sub-classes or concrete callers can + * down-cast subsystems[i] to the typed sub-system and call its own addBodyIntegral. This is done + * via the template setMaterial below. + */ +struct CombinedSystem : public SystemBase { + std::vector> subsystems; ///< Ordered sub-systems solved during each staggered sweep. + + /// @brief Construct a CombinedSystem. weak_forms is populated by combineSystems. + using SystemBase::SystemBase; + + /** + * @brief Set a coupled material on all sub-systems. + * + * Calls setMaterial on each sub-system if that sub-system is of the given typed pointer + * (down-cast via dynamic_pointer_cast). Intended for tight-coupling materials that register + * integrands on both sub-systems' weak forms. + * + * @tparam SubSystemType The concrete type that exposes setMaterial. + * @tparam MaterialType The material type. + */ + template + void setMaterialOn(std::shared_ptr sub, const MaterialType& material, const std::string& domain_name) + { + sub->setMaterial(material, domain_name); + } +}; + +/** + * @brief Combine two or more independently-built sub-systems into a CombinedSystem. + * + * Preconditions: + * - All sub-systems share the same FieldStore (built via registerXxxFields + buildXxxFromStore). + * - Sub-system weak_forms are already populated (registerXxx was called before buildXxx). + * + * @param subs Two or more sub-systems that share a FieldStore. + */ +template +auto combineSystems(std::shared_ptr... subs) +{ + static_assert(sizeof...(subs) >= 2, "combineSystems requires at least two sub-systems"); + + auto first_sub = std::get<0>(std::forward_as_tuple(subs...)); + auto field_store = first_sub->field_store; + + auto combined = std::make_shared(); + combined->field_store = field_store; + + int max_stagger_iters = 1; + bool exact_staggered_steps = false; + + std::vector> post_solve_systems; + std::vector> subsystem_global_block_indices; + + ( + [&](auto& sub) { + std::vector global_block_indices; + combined->subsystems.push_back(sub); + for (auto& wf : sub->weak_forms) { + global_block_indices.push_back(combined->weak_forms.size()); + combined->weak_forms.push_back(wf); + } + subsystem_global_block_indices.push_back(global_block_indices); + if (sub->solver) { + max_stagger_iters = std::max(max_stagger_iters, sub->solver->maxStaggeredIterations()); + exact_staggered_steps = exact_staggered_steps || sub->solver->exactStaggeredSteps(); + } + for (auto& cz_sys : sub->cycle_zero_systems) { + combined->cycle_zero_systems.push_back(cz_sys); + } + post_solve_systems.insert(post_solve_systems.end(), sub->post_solve_systems.begin(), + sub->post_solve_systems.end()); + }(subs), + ...); + + combined->solver = std::make_shared(max_stagger_iters, exact_staggered_steps); + for (size_t i = 0; i < combined->subsystems.size(); ++i) { + const auto& sub = combined->subsystems[i]; + SLIC_ERROR_IF(!sub->solver, "Combined subsystem must have a solver"); + combined->solver->appendStagesWithBlockMapping(*sub->solver, subsystem_global_block_indices[i]); + } + + combined->post_solve_systems = post_solve_systems; + + return combined; +} + +/** + * @brief Combine two or more independently-built sub-systems into a single monolithic SystemBase. + * + * All sub-system weak forms are concatenated and solved simultaneously by @p solver. + * + * Preconditions: + * - All sub-systems share the same FieldStore. + * - Sub-system weak_forms are already populated. + * + * @param solver The monolithic SystemSolver that will solve the combined block system, + * including the aggregated cycle-zero system if any sub-systems have one. + * @param subs Two or more sub-systems that share a FieldStore. + */ +template +std::shared_ptr combineSystems(std::shared_ptr solver, std::shared_ptr... subs) +{ + static_assert(sizeof...(subs) >= 2, "combineSystems requires at least two sub-systems"); + + auto field_store = std::get<0>(std::forward_as_tuple(subs...))->field_store; + + std::vector> wfs; + std::vector> cycle_zero_systems; + std::vector> post_solve_systems; + + ( + [&](auto& sub) { + for (auto& wf : sub->weak_forms) { + wfs.push_back(wf); + } + for (auto& cz_sys : sub->cycle_zero_systems) { + cycle_zero_systems.push_back(cz_sys); + } + post_solve_systems.insert(post_solve_systems.end(), sub->post_solve_systems.begin(), + sub->post_solve_systems.end()); + }(subs), + ...); + + auto combined = std::make_shared(field_store, solver, wfs); + combined->cycle_zero_systems = cycle_zero_systems; + combined->post_solve_systems = post_solve_systems; + + return combined; +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/coupling_params.hpp b/src/smith/differentiable_numerics/coupling_params.hpp new file mode 100644 index 0000000000..af7ba3b555 --- /dev/null +++ b/src/smith/differentiable_numerics/coupling_params.hpp @@ -0,0 +1,320 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file coupling_params.hpp + * @brief Coupling pack types and helpers for injecting explicit coupled-physics fields into weak form parameter packs. + * + * Builders accept at most two optional trailing arguments after `self_fields`: + * 1. `couplingFields(coupled_physics_fields...)` — coupled physics contributions + * 2. `param_fields` (a `ParamFields<...>`) — registered user parameter fields (must be last) + * + * Tail of each material/source closure: `(coupling_fields..., parameter_fields...)`. + */ + +#pragma once + +#include +#include +#include + +#include "smith/differentiable_numerics/field_store.hpp" +#include "smith/differentiable_numerics/system_base.hpp" + +namespace smith { + +/** + * @brief Fields returned by a physics register function, carrying time rule type information. + * + * Doubles as a per-physics coupling segment: when supplied via `couplingFields(...)`, the + * builder interpolates `TimeRule::num_states` raw arguments before passing the values to + * the user callback. + */ +template +struct PhysicsFields { + using time_rule_type = TimeRule; ///< The time integration rule type. + static constexpr int dim = Dim; ///< Spatial dimension. + static constexpr int order = Order; ///< Spatial order. + static constexpr std::size_t num_fields = sizeof...(Spaces); ///< Number of fields. + std::shared_ptr field_store; ///< Pointer to the field store. + std::tuple...> fields; ///< The fields. + + /// Constructor + PhysicsFields(std::shared_ptr fs, FieldType... f) + : field_store(std::move(fs)), fields(std::move(f)...) + { + } +}; + +/** + * @brief Registered parameter-only field bundle. + */ +template +struct ParamFields { + static constexpr std::size_t num_fields = sizeof...(Spaces); ///< Number of fields. + std::tuple...> fields; ///< The fields. + /// Constructor + ParamFields(FieldType... fs) : fields(std::move(fs)...) {} +}; + +/// Deduction guide for ParamFields +template +ParamFields(FieldType...) -> ParamFields; + +/** + * @brief Bundle of coupled `PhysicsFields` packs supplied to a builder as a single coupling arg. + * + * Order is preserved. Each entry contributes its fields and an interpolation segment governed + * by its own `time_rule_type`. + */ +template +struct CouplingFields { + std::tuple packs; ///< The coupling packs. +}; + +namespace detail { + +template +struct is_physics_fields_impl : std::false_type {}; + +template +struct is_physics_fields_impl> : std::true_type {}; + +/// True when T is a PhysicsFields pack. +template +inline constexpr bool is_physics_fields_v = is_physics_fields_impl>::value; + +} // namespace detail + +/// Helper to construct a CouplingFields bundle +template +auto couplingFields(const PFs&... pfs) +{ + static_assert((detail::is_physics_fields_v && ...), "couplingFields(...) only accepts PhysicsFields packs"); + return CouplingFields{std::make_tuple(pfs...)}; +} + +/** + * @brief Register parameter fields as type-level tokens. + */ +template +auto registerParameterFields(const std::shared_ptr& field_store, FieldType... param_types) +{ + auto register_one = [&](auto param_type) { + param_type.name = "param_" + param_type.name; + field_store->addParameter(param_type); + return param_type; + }; + return ParamFields{register_one(std::move(param_types))...}; +} + +namespace detail { + +/// True for a `std::tuple` returned by `collectCouplingFields`. +template +struct is_coupling_packs_impl : std::false_type {}; + +template +struct is_coupling_packs_impl> : std::true_type {}; + +/// @brief True if T is a tuple of coupling packs. +template +inline constexpr bool is_coupling_packs_v = is_coupling_packs_impl>::value; + +/// @brief Base case: T does not have a time rule. +template +inline constexpr bool has_time_rule_v = false; + +/// @brief True if T is a PhysicsFields type. +template +inline constexpr bool has_time_rule_v>> = true; + +// ------------------------------------------------------------------------- +// Trailing-arg extraction +// ------------------------------------------------------------------------- + +/// Concatenate each pack's `.fields` tuple — used to derive trailing weak-form parameter spaces. +template +auto flattenCouplingFields(const PacksTuple& packs) +{ + return std::apply([](const auto&... pack) { return std::tuple_cat(pack.fields...); }, packs); +} + +/// @brief Collect no coupling or parameter packs. +inline auto collectCouplingFields() { return std::tuple<>{}; } + +template +/// @brief Collect only coupled physics packs. +auto collectCouplingFields(const CouplingFields& coupled) +{ + return coupled.packs; +} + +template +/// @brief Collect only registered parameter fields. +auto collectCouplingFields(const ParamFields& params) +{ + return std::make_tuple(params); +} + +template +/// @brief Collect coupled physics packs followed by registered parameter fields. +auto collectCouplingFields(const CouplingFields& coupled, const ParamFields& params) +{ + return std::tuple_cat(coupled.packs, std::make_tuple(params)); +} + +// ------------------------------------------------------------------------- +// Time-rule interpolation +// ------------------------------------------------------------------------- + +/// @brief Evaluate a single coupling pack's time rule. +template +auto evaluateCouplingPack(const Pack& /*pack*/, const TimeInfoT& t_info, const RawTuple& raw_args, + std::index_sequence) +{ + if constexpr (is_physics_fields_v) { + using Rule = typename Pack::time_rule_type; + Rule rule; + return rule.interpolate(t_info, std::get(raw_args)...); + } else { + return std::forward_as_tuple(std::get(raw_args)...); + } +} + +/// @brief Evaluate all coupling packs over their corresponding raw arguments. +template +auto evaluateCouplingPacks(const PacksTuple& packs, const TimeInfoT& t_info, const RawTuple& raw_args) +{ + if constexpr (I == std::tuple_size_v>) { + return std::tuple{}; + } else { + const auto& pack = std::get(packs); + using Pack = std::decay_t; + auto head = evaluateCouplingPack(pack, t_info, raw_args, std::make_index_sequence{}); + auto tail = evaluateCouplingPacks(packs, t_info, raw_args); + return std::tuple_cat(head, tail); + } +} + +/** + * @brief Interpolate self time-rule states then coupling segments, then invoke callback. + * + * Callback signature: `(self_states..., interpolated_coupling...)`. + * + * Raw arguments start with the self time-rule states. Remaining arguments are split by + * coupling pack, and each pack's time rule interpolates its own segment. + */ +template +decltype(auto) applyTimeRuleAndCoupling(const Rule& rule, const Coupling& coupling, const TimeInfoT& t_info, + Callback&& callback, const RawArgs&... raw_args) +{ + static_assert(sizeof...(RawArgs) >= Rule::num_states, "Not enough raw arguments for time-rule interpolation"); + auto raw_tuple = std::forward_as_tuple(raw_args...); + auto self_states = [&](std::index_sequence) { + return rule.interpolate(t_info, std::get(raw_tuple)...); + }(std::make_index_sequence{}); + auto coupling_states = evaluateCouplingPacks<0, Rule::num_states>(coupling, t_info, raw_tuple); + auto callback_args = std::tuple_cat(self_states, coupling_states); + return std::apply(std::forward(callback), callback_args); +} + +// ------------------------------------------------------------------------- +// Type-level coupling-space extraction (used by weak-form parameter type computation) +// ------------------------------------------------------------------------- + +/// @brief Flatten a `tuple` type into `Parameters`. +template +struct FlattenCoupling; + +/// @brief Converts a `std::tuple<...>` of spaces into `Parameters<...>`. +template +struct TupleToParameters; + +/// @brief Specialization of TupleToParameters for a tuple of spaces. +template +struct TupleToParameters> { + using type = Parameters; ///< The converted parameter pack. +}; + +/// @brief Typedef for converting a tuple of spaces into `Parameters<...>`. +template +using tuple_to_parameters_t = typename TupleToParameters>::type; + +/// @brief Maps a coupling pack type to a `std::tuple<...>` of its spaces. +template +struct pack_tuple; + +/// @brief Specialization of pack_tuple for physics coupling fields. +template +struct pack_tuple> { + using type = std::tuple; ///< The coupling spaces as a tuple. +}; + +/// @brief Specialization of pack_tuple for parameter-only fields. +template +struct pack_tuple> { + using type = std::tuple; ///< The parameter spaces as a tuple. +}; + +/// @brief Typedef for extracting a tuple of spaces from a coupling pack. +template +using pack_tuple_t = typename pack_tuple>::type; + +/// @brief Typedef for concatenating space tuples with `std::tuple_cat`. +template +using tuple_cat_t = decltype(std::tuple_cat(std::declval()...)); + +/// @brief Appends coupling parameter spaces to an existing `Parameters<...>` list. +template +struct AppendParameters; + +/// @brief Specialization of AppendParameters for two `Parameters<...>` packs. +template +struct AppendParameters, Parameters> { + using type = Parameters; ///< The appended parameter list. +}; + +/// @brief Specialization of FlattenCoupling for a tuple of coupling packs. +template +struct FlattenCoupling> { + public: + using tuple_type = tuple_cat_t...>; ///< The flattened tuple of coupling spaces. + using parameters = tuple_to_parameters_t; ///< Flattened parameter spaces. +}; + +/// @brief Typedef for flattened coupling parameter spaces. +template +using flatten_coupling_t = typename FlattenCoupling>::parameters; + +/// @brief Type trait to construct `Parameters` for a weak form's parameter list. +template +struct TimeRuleParamsImpl; + +/// @brief Specialization of TimeRuleParamsImpl. +template +struct TimeRuleParamsImpl> { + using type = smith::TimeRuleParams; ///< The constructed TimeRuleParams type. +}; + +/// @brief Typedef for TimeRuleParams. +template +using TimeRuleParams = typename TimeRuleParamsImpl>::type; + +/// @brief Type trait to append coupling parameter spaces to fixed parameters. +template +struct AppendCouplingToParams; + +/// @brief Specialization of AppendCouplingToParams. +template +struct AppendCouplingToParams> { + using type = typename AppendParameters, Parameters>::type; + ///< The appended parameter list. +}; + +} // namespace detail + +} // namespace smith diff --git a/src/smith/differentiable_numerics/differentiable_physics.cpp b/src/smith/differentiable_numerics/differentiable_physics.cpp index b0b153f60a..46892b2042 100644 --- a/src/smith/differentiable_numerics/differentiable_physics.cpp +++ b/src/smith/differentiable_numerics/differentiable_physics.cpp @@ -119,9 +119,13 @@ const FiniteElementState& DifferentiablePhysics::state([[maybe_unused]] const st const FiniteElementDual& DifferentiablePhysics::dual(const std::string& reaction_name) const { - SLIC_ERROR_IF(reaction_name_to_reaction_index_.find(reaction_name) == reaction_name_to_reaction_index_.end(), - axom::fmt::format("Could not find reaction named {0} in mesh with tag \"{1}\" to get", reaction_name, - mesh_->tag())); + if (reaction_name_to_reaction_index_.find(reaction_name) == reaction_name_to_reaction_index_.end()) { + std::string available; + for (auto& n : reaction_names_) available += n + " "; + SLIC_ERROR(axom::fmt::format( + "Could not find reaction named {0} in mesh with tag \"{1}\" to get. Available reactions (size {2}): {3}", + reaction_name, mesh_->tag(), reaction_names_.size(), available)); + } size_t reaction_index = reaction_name_to_reaction_index_.at(reaction_name); SLIC_ERROR_IF(reaction_states_.empty() && !reaction_names_.empty(), @@ -316,6 +320,27 @@ std::vector DifferentiablePhysics::getFieldStatesAndParamStates() co return fields; } +FieldState DifferentiablePhysics::getInitialFieldState(const std::string& state_name) const +{ + SLIC_ERROR_IF(state_name_to_field_index_.find(state_name) == state_name_to_field_index_.end(), + std::format("Could not find initial field named {0}", state_name)); + return initial_field_states_[state_name_to_field_index_.at(state_name)]; +} + +FieldState DifferentiablePhysics::getFieldState(const std::string& state_name) const +{ + SLIC_ERROR_IF(state_name_to_field_index_.find(state_name) == state_name_to_field_index_.end(), + std::format("Could not find field named {0}", state_name)); + return field_states_[state_name_to_field_index_.at(state_name)]; +} + +FieldState DifferentiablePhysics::getFieldParam(const std::string& param_name) const +{ + SLIC_ERROR_IF(param_name_to_field_index_.find(param_name) == param_name_to_field_index_.end(), + std::format("Could not find parameter named {0}", param_name)); + return field_params_[param_name_to_field_index_.at(param_name)]; +} + FieldState DifferentiablePhysics::getShapeDispFieldState() const { return *field_shape_displacement_; } void DifferentiablePhysics::initializeReactionStates() diff --git a/src/smith/differentiable_numerics/differentiable_physics.hpp b/src/smith/differentiable_numerics/differentiable_physics.hpp index 1820e95036..1ff8dbf1b5 100644 --- a/src/smith/differentiable_numerics/differentiable_physics.hpp +++ b/src/smith/differentiable_numerics/differentiable_physics.hpp @@ -16,6 +16,7 @@ #include "smith/physics/base_physics.hpp" #include "smith/differentiable_numerics/field_state.hpp" #include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" #include #include @@ -138,13 +139,22 @@ class DifferentiablePhysics : public BasePhysics { /// @return Copies of the tracked initial state fields. std::vector getInitialFieldStates() const { return initial_field_states_; } + /// @brief Get a tracked initial state field by name. + FieldState getInitialFieldState(const std::string& state_name) const; + /// @brief Get the current primal state fields. /// @return Copies of the tracked state fields at the current cycle. std::vector getFieldStates() const { return field_states_; } + /// @brief Get a tracked current state field by name. + FieldState getFieldState(const std::string& state_name) const; + /// @brief Get all the parameter FieldStates std::vector getFieldParams() const { return field_params_; } + /// @brief Get a tracked parameter field by name. + FieldState getFieldParam(const std::string& param_name) const; + /// @brief Get the tracked state fields followed by the tracked parameter fields. /// @return A concatenated vector of state fields then parameter fields. std::vector getFieldStatesAndParamStates() const; @@ -197,4 +207,37 @@ class DifferentiablePhysics : public BasePhysics { 0; ///< previous cycle, saved to reconstruct the start of step time used in computing reaction forces }; +template +/** + * @brief Build a `DifferentiablePhysics` from a preconfigured system and advancer. + * + * @param system System whose field store owns states, parameters, mesh, and reactions. + * @param advancer Time integrator used for forward solves. + * @param physics_name Name exposed through the `BasePhysics` interface. + */ +std::unique_ptr makeDifferentiablePhysics(std::shared_ptr system, + std::shared_ptr advancer, + const std::string& physics_name) +{ + return std::make_unique( + system->field_store->getMesh(), system->field_store->graph(), system->field_store->getShapeDisp(), + system->field_store->getStateFields(), system->field_store->getParameterFields(), std::move(advancer), + physics_name, system->field_store->getReactionInfos()); +} + +template +/** + * @brief Build a `DifferentiablePhysics` and default multiphysics advancer from a system. + * + * Uses cycle-zero and post-solve systems already attached to `system`. + * + * @param system Main system to wrap. + * @param physics_name Name exposed through the `BasePhysics` interface. + */ +std::unique_ptr makeDifferentiablePhysics(std::shared_ptr system, + const std::string& physics_name) +{ + return makeDifferentiablePhysics(system, makeAdvancer(system), physics_name); +} + } // namespace smith diff --git a/src/smith/differentiable_numerics/differentiable_test_utils.hpp b/src/smith/differentiable_numerics/differentiable_test_utils.hpp index 76b4e87700..4ecf7e79d2 100644 --- a/src/smith/differentiable_numerics/differentiable_test_utils.hpp +++ b/src/smith/differentiable_numerics/differentiable_test_utils.hpp @@ -14,67 +14,10 @@ #include "gretl/double_state.hpp" #include "smith/differentiable_numerics/field_state.hpp" -#include "smith/physics/scalar_objective.hpp" #include "smith/physics/boundary_conditions/boundary_condition_manager.hpp" namespace smith { -/// @brief Utility function to construct a smith::functional which evaluates the total kinetic energy -template -auto createKineticEnergyIntegrator(smith::Domain& domain, const mfem::ParFiniteElementSpace& velocity_space, - const mfem::ParFiniteElementSpace& density_space) -{ - static constexpr int dim = DispSpace::components; - auto ke_integrator = std::make_shared>( - std::array{&velocity_space, &velocity_space, &density_space}); - ke_integrator->AddDomainIntegral( - Dimension{}, DependsOn<0, 1, 2>{}, - [&](auto /*t*/, auto /*X*/, auto U, auto V, auto Rho) { - auto rho = get(Rho); - auto v = get(V); - auto ke = 0.5 * rho * inner(v, v); - auto dx_dX = get(U) + Identity(); - auto J = det(dx_dX); - return ke * J; - }, - domain); - return ke_integrator; -} - -/// @brief Utility function which computes the kinetic energy and returns it as a gretl state (with its vjp defined) -template -gretl::State computeKineticEnergy( - const std::shared_ptr>& energy_func, - smith::FieldState disp, smith::FieldState velo, smith::FieldState density, double scaling) -{ - return gretl::create_state( - // specify how to zero the dual - [](double forwardVal) { return 0 * forwardVal; }, - // define how to (re)evaluate the output - [=](const smith::FEFieldPtr& Disp, const smith::FEFieldPtr& Velo, const smith::FEFieldPtr& Density) -> double { - return (*energy_func)(0.0, *Disp, *Velo, *Density) * scaling; - }, - // define how to backpropagate the vjp - [=](const smith::FEFieldPtr& Disp, const smith::FEFieldPtr& Velo, const smith::FEFieldPtr& Density, - const double& /*ke*/, smith::FEDualPtr& Disp_dual, smith::FEDualPtr& Velo_dual, - smith::FEDualPtr& Density_dual, const double& ke_dual) -> void { - auto ddisp = (*energy_func)(0.0, smith::differentiate_wrt(*Disp), *Velo, *Density); - auto de_ddisp = assemble(smith::get(ddisp)); - - auto dvelo = (*energy_func)(0.0, *Disp, smith::differentiate_wrt(*Velo), *Density); - auto de_dvelo = assemble(smith::get(dvelo)); - - auto ddens = (*energy_func)(0.0, *Disp, *Velo, smith::differentiate_wrt(*Density)); - auto de_ddensity = assemble(smith::get(ddens)); - - Disp_dual->Add(scaling * ke_dual, *de_ddisp); - Velo_dual->Add(scaling * ke_dual, *de_dvelo); - Density_dual->Add(scaling * ke_dual, *de_ddensity); - }, - // give the input values - disp, velo, density); -} - /// @brief testing utility to confirm order of convergence of the finite differences relative to the backprop gradient inline auto checkGradients(const gretl::State& objectiveState, FieldState& inputState, FiniteElementDual& inputDual, double objectiveBase, gretl::DataStore& dataStore, double eps) diff --git a/src/smith/differentiable_numerics/dirichlet_boundary_conditions.cpp b/src/smith/differentiable_numerics/dirichlet_boundary_conditions.cpp index 0834dc9785..fce104e9aa 100644 --- a/src/smith/differentiable_numerics/dirichlet_boundary_conditions.cpp +++ b/src/smith/differentiable_numerics/dirichlet_boundary_conditions.cpp @@ -6,12 +6,76 @@ #include "smith/physics/mesh.hpp" #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" +#include "smith/physics/boundary_conditions/boundary_condition_manager.hpp" +#include "smith/physics/boundary_conditions/boundary_condition.hpp" + +#include +#include namespace smith { +namespace { + +constexpr double bc_time_fd_step = 1.0e-4; + +class SecondTimeDerivativeScalarCoefficient : public mfem::Coefficient { + public: + explicit SecondTimeDerivativeScalarCoefficient(std::shared_ptr source) : source_(std::move(source)) + { + } + + double Eval(mfem::ElementTransformation& T, const mfem::IntegrationPoint& ip) override + { + const double t0 = GetTime(); + const double h = bc_time_fd_step * std::max(1.0, std::abs(t0)); + source_->SetTime(t0); + const double f0 = source_->Eval(T, ip); + source_->SetTime(t0 + h); + const double f1 = source_->Eval(T, ip); + source_->SetTime(t0 + 2.0 * h); + const double f2 = source_->Eval(T, ip); + source_->SetTime(t0); + return (f2 - 2.0 * f1 + f0) / (h * h); + } + + private: + std::shared_ptr source_; +}; + +class SecondTimeDerivativeVectorCoefficient : public mfem::VectorCoefficient { + public: + explicit SecondTimeDerivativeVectorCoefficient(std::shared_ptr source) + : mfem::VectorCoefficient(source->GetVDim()), source_(std::move(source)) + { + } + + void Eval(mfem::Vector& V, mfem::ElementTransformation& T, const mfem::IntegrationPoint& ip) override + { + const double t0 = GetTime(); + const double h = bc_time_fd_step * std::max(1.0, std::abs(t0)); + mfem::Vector v0(vdim), v1(vdim), v2(vdim); + source_->SetTime(t0); + source_->Eval(v0, T, ip); + source_->SetTime(t0 + h); + source_->Eval(v1, T, ip); + source_->SetTime(t0 + 2.0 * h); + source_->Eval(v2, T, ip); + source_->SetTime(t0); + V = v2; + V.Add(-2.0, v1); + V += v0; + V /= (h * h); + } + + private: + std::shared_ptr source_; +}; + +} // namespace + DirichletBoundaryConditions::DirichletBoundaryConditions(const mfem::ParMesh& mfem_mesh, mfem::ParFiniteElementSpace& space) - : bcs_(mfem_mesh), space_(space) + : mfem_mesh_(mfem_mesh), bcs_(mfem_mesh), space_(space) { } @@ -20,4 +84,26 @@ DirichletBoundaryConditions::DirichletBoundaryConditions(const Mesh& mesh, mfem: { } +const BoundaryConditionManager& DirichletBoundaryConditions::getSecondDerivativeManager() const +{ + second_derivative_manager_.emplace(mfem_mesh_); + rebuildSecondDerivativeManager(*second_derivative_manager_); + return *second_derivative_manager_; +} + +void DirichletBoundaryConditions::rebuildSecondDerivativeManager(BoundaryConditionManager& target) const +{ + for (const auto& bc : bcs_.essentials()) { + if (is_vector_valued(bc.coefficient())) { + auto deriv_coef = std::make_shared( + get>(bc.coefficient())); + target.addEssentialByTrueDofs(bc.getTrueDofList(), deriv_coef, space_); + } else { + auto deriv_coef = std::make_shared( + get>(bc.coefficient())); + target.addEssential(bc.getLocalDofList(), deriv_coef, space_, bc.component()); + } + } +} + } // namespace smith diff --git a/src/smith/differentiable_numerics/dirichlet_boundary_conditions.hpp b/src/smith/differentiable_numerics/dirichlet_boundary_conditions.hpp index 3848af4aa3..7acc8678e4 100644 --- a/src/smith/differentiable_numerics/dirichlet_boundary_conditions.hpp +++ b/src/smith/differentiable_numerics/dirichlet_boundary_conditions.hpp @@ -12,6 +12,8 @@ #pragma once +#include + #include "smith/physics/boundary_conditions/boundary_condition_manager.hpp" namespace smith { @@ -158,12 +160,26 @@ class DirichletBoundaryConditions { setFixedVectorBCs(domain, components); } - /// @brief Return the smith BoundaryConditionManager + /// @brief Return the value-level smith BoundaryConditionManager. const smith::BoundaryConditionManager& getBoundaryConditionManager() const { return bcs_; } + /// @brief Return the BC manager for acceleration and other second-time-derivative fields. + /// + /// For a value-level essential boundary condition @c u_b(t), this manager constrains a dependent + /// second-derivative field to @c u_b''(t), computed by one-sided three-point forward FD with step + /// @c h = 1e-4 * max(1,|t|). + /// + /// Rebuilt on each call from current value-level essentials, so late additions are reflected. + const smith::BoundaryConditionManager& getSecondDerivativeManager() const; + private: + void rebuildSecondDerivativeManager(BoundaryConditionManager& target) const; + + const mfem::ParMesh& mfem_mesh_; ///< for constructing derivative-level managers smith::BoundaryConditionManager bcs_; ///< boundary condition manager that does the heavy lifting mfem::ParFiniteElementSpace& space_; ///< save the space for the field which will be constrained + + mutable std::optional second_derivative_manager_; }; } // namespace smith diff --git a/src/smith/differentiable_numerics/evaluate_objective.cpp b/src/smith/differentiable_numerics/evaluate_objective.cpp index c5dd4e17a2..9d57a4970e 100644 --- a/src/smith/differentiable_numerics/evaluate_objective.cpp +++ b/src/smith/differentiable_numerics/evaluate_objective.cpp @@ -29,14 +29,17 @@ DoubleState evaluateObjective(const ScalarObjective& objective, const FieldState }); value.set_vjp([time_info, objective_ptr](gretl::UpstreamStates& upstreams, const gretl::DownstreamState& downstream) { + const double downstream_dual = downstream.get_dual(); + if (downstream_dual == 0.0) { + return; + } + auto shape = upstreams[0].get(); std::vector fields; for (size_t i = 1; i < upstreams.size(); ++i) { fields.push_back(upstreams[i].get()); } - const double downstream_dual = downstream.get_dual(); - upstreams[0].get_dual()->Add( downstream_dual, objective_ptr->mesh_coordinate_gradient(time_info, shape.get(), getConstFieldPointers(fields))); diff --git a/src/smith/differentiable_numerics/field_store.cpp b/src/smith/differentiable_numerics/field_store.cpp index 6790474be9..20d0869ade 100644 --- a/src/smith/differentiable_numerics/field_store.cpp +++ b/src/smith/differentiable_numerics/field_store.cpp @@ -11,16 +11,24 @@ namespace smith { -FieldStore::FieldStore(std::shared_ptr mesh, size_t storage_size) +FieldStore::FieldStore(std::shared_ptr mesh, size_t storage_size, std::string prepend_name) : mesh_(mesh), - graph_(std::make_shared(std::make_unique(storage_size))) + graph_(std::make_shared(std::make_unique(storage_size))), + prepend_name_(std::move(prepend_name)) { } +std::string FieldStore::prefix(const std::string& base) const +{ + if (prepend_name_.empty()) { + return base; + } + return prepend_name_ + "_" + base; +} + std::shared_ptr FieldStore::addBoundaryConditions(FEFieldPtr field) { - boundary_conditions_.push_back(std::make_shared(mesh_->mfemParMesh(), field->space())); - return boundary_conditions_.back(); + return std::make_shared(mesh_->mfemParMesh(), field->space()); } void FieldStore::addWeakFormUnknownArg(std::string weak_form_name, std::string argument_name, size_t argument_index) @@ -58,47 +66,141 @@ void FieldStore::printMap() std::vector> FieldStore::indexMap(const std::vector& residual_names) const { - std::vector> block_indices(residual_names.size()); + // Build a local column space: each residual in the subsystem contributes one local column, + // corresponding to its "self" diagonal unknown. The self-unknown is preferably the residual's + // reaction (test) field if that field appears in the unknown-arg list for this weak form; + // otherwise fall back on the first unknown argument (handles cases like the cycle-zero + // acceleration solve, where the reaction field is a dependent/history field). + std::map global_state_to_local_col; + for (size_t res_i = 0; res_i < residual_names.size(); ++res_i) { + const std::string& res_name = residual_names[res_i]; + size_t global_state_idx = invalid_block_index; + + std::string reaction_name; + for (const auto& kv : weak_form_to_test_field_) { + if (kv.first == res_name) { + reaction_name = kv.second; + break; + } + } + // Check if the reaction field is one of the registered unknown args for this weak form. + bool reaction_is_unknown = false; + if (!reaction_name.empty() && weak_form_name_to_unknown_name_index_.count(res_name)) { + for (const auto& label : weak_form_name_to_unknown_name_index_.at(res_name)) { + if (label.field_name == reaction_name) { + reaction_is_unknown = true; + break; + } + } + } + + if (reaction_is_unknown) { + global_state_idx = to_states_index_.at(reaction_name); + } else { + const auto& arg_info = weak_form_name_to_unknown_name_index_.at(res_name); + SLIC_ERROR_IF(arg_info.empty(), + "Weak form '" << res_name << "' has no unknown arguments; cannot build index map."); + global_state_idx = to_states_index_.at(arg_info.front().field_name); + } + global_state_to_local_col[global_state_idx] = res_i; + } + + std::vector> block_indices(residual_names.size()); for (size_t res_i = 0; res_i < residual_names.size(); ++res_i) { std::vector& res_indices = block_indices[res_i]; - res_indices = std::vector(num_unknowns_, invalid_block_index); + res_indices = std::vector(residual_names.size(), invalid_block_index); const std::string& res_name = residual_names[res_i]; const auto& arg_info = weak_form_name_to_unknown_name_index_.at(res_name); for (const auto& field_name_and_arg_index : arg_info) { - const std::string field_name = field_name_and_arg_index.field_name; - size_t unknown_index = to_unknown_index_.at(field_name); - SLIC_ASSERT(unknown_index < num_unknowns_); - res_indices[unknown_index] = field_name_and_arg_index.field_index_in_residual; + size_t global_state_index = to_states_index_.at(field_name_and_arg_index.field_name); + auto it = global_state_to_local_col.find(global_state_index); + if (it != global_state_to_local_col.end()) { + res_indices[it->second] = field_name_and_arg_index.field_index_in_residual; + } + // else: field belongs to a different subsystem; treat as fixed input here. } } return block_indices; } -std::vector FieldStore::getBoundaryConditionManagers() const +std::vector FieldStore::getBoundaryConditionManagers( + const std::vector& weak_form_names) const +{ + std::vector field_names; + field_names.reserve(weak_form_names.size()); + for (const auto& wf_name : weak_form_names) { + field_names.push_back(getWeakFormReaction(wf_name)); + } + return getBoundaryConditionManagersForFields(field_names); +} + +std::vector FieldStore::getBoundaryConditionManagersForFields( + const std::vector& field_names) const { + struct BoundaryConditionRef { + std::string primary_name; + bool use_second_derivative; + }; + std::map field_to_primary; + for (const auto& [_rule, mapping] : time_integration_rules_) { + if (!mapping.primary_name.empty()) { + field_to_primary[mapping.primary_name] = {mapping.primary_name, false}; + } + if (!mapping.history_name.empty()) { + field_to_primary[mapping.history_name] = {mapping.primary_name, false}; + } + if (!mapping.ddot_name.empty()) { + field_to_primary[mapping.ddot_name] = {mapping.primary_name, true}; + } + } + std::vector bcs; - for (auto& bc : boundary_conditions_) { - bcs.push_back(&bc->getBoundaryConditionManager()); + for (const auto& field_name : field_names) { + // Direct DBC entry takes precedence (e.g. an independent unknown like stress with its own BC). + auto direct = boundary_conditions_.find(field_name); + if (direct != boundary_conditions_.end()) { + bcs.push_back(&direct->second->getBoundaryConditionManager()); + continue; + } + + // Otherwise resolve via the time-integration mapping that owns this reaction field. + auto ref_it = field_to_primary.find(field_name); + if (ref_it == field_to_primary.end()) { + bcs.push_back(nullptr); + continue; + } + auto primary_it = boundary_conditions_.find(ref_it->second.primary_name); + if (primary_it == boundary_conditions_.end()) { + bcs.push_back(nullptr); + continue; + } + const auto& dbc = *primary_it->second; + bcs.push_back(ref_it->second.use_second_derivative ? &dbc.getSecondDerivativeManager() + : &dbc.getBoundaryConditionManager()); } return bcs; } -std::shared_ptr FieldStore::getBoundaryConditions(size_t unknown_index) const +bool FieldStore::hasField(const std::string& field_name) const { - SLIC_ERROR_IF(unknown_index >= boundary_conditions_.size(), "Unknown index out of bounds for boundary conditions"); - return boundary_conditions_[unknown_index]; + const auto resolved_name = resolveFieldName(field_name); + if (to_states_index_.count(resolved_name)) return true; + if (to_params_index_.count(resolved_name)) return true; + if (!shape_disp_.empty() && shape_disp_[0].get()->name() == resolved_name) return true; + return false; } size_t FieldStore::getFieldIndex(const std::string& field_name) const { - if (to_states_index_.count(field_name)) { - return to_states_index_.at(field_name); + const auto resolved_name = resolveFieldName(field_name); + if (to_states_index_.count(resolved_name)) { + return to_states_index_.at(resolved_name); } - if (to_params_index_.count(field_name)) { - return to_params_index_.at(field_name); + if (to_params_index_.count(resolved_name)) { + return to_params_index_.at(resolved_name); } SLIC_ERROR("Field or parameter '" << field_name << "' not found in getFieldIndex"); return 0; // unreachable @@ -106,14 +208,15 @@ size_t FieldStore::getFieldIndex(const std::string& field_name) const FieldState FieldStore::getField(const std::string& field_name) const { + const auto resolved_name = resolveFieldName(field_name); // Check if it's a state field - if (to_states_index_.count(field_name)) { - size_t field_index = to_states_index_.at(field_name); + if (to_states_index_.count(resolved_name)) { + size_t field_index = to_states_index_.at(resolved_name); return states_[field_index]; } // Otherwise check if it's a parameter - if (to_params_index_.count(field_name)) { - size_t param_index = to_params_index_.at(field_name); + if (to_params_index_.count(resolved_name)) { + size_t param_index = to_params_index_.at(resolved_name); return params_[param_index]; } SLIC_ERROR("Field or parameter '" << field_name << "' not found"); @@ -122,14 +225,49 @@ FieldState FieldStore::getField(const std::string& field_name) const FieldState FieldStore::getParameter(const std::string& param_name) const { - size_t param_index = to_params_index_.at(param_name); + const auto resolved_name = resolveFieldName(param_name); + size_t param_index = to_params_index_.at(resolved_name); return params_[param_index]; } void FieldStore::setField(const std::string& field_name, FieldState updated_field) { - size_t field_index = getFieldIndex(field_name); - states_[field_index] = updated_field; + const auto resolved_name = resolveFieldName(field_name); + if (to_states_index_.count(resolved_name)) { + states_[to_states_index_.at(resolved_name)] = updated_field; + return; + } + if (to_params_index_.count(resolved_name)) { + params_[to_params_index_.at(resolved_name)] = updated_field; + return; + } + if (!shape_disp_.empty() && shape_disp_[0].get()->name() == resolved_name) { + shape_disp_[0] = updated_field; + return; + } + SLIC_ERROR("Field '" << field_name << "' not found in setField"); +} + +std::string FieldStore::resolveFieldName(const std::string& field_name) const +{ + if (to_states_index_.count(field_name) || to_params_index_.count(field_name)) { + return field_name; + } + if (!shape_disp_.empty() && shape_disp_[0].get()->name() == field_name) { + return field_name; + } + + const auto prefixed_name = prefix(field_name); + if (prefixed_name != field_name) { + if (to_states_index_.count(prefixed_name) || to_params_index_.count(prefixed_name)) { + return prefixed_name; + } + if (!shape_disp_.empty() && shape_disp_[0].get()->name() == prefixed_name) { + return prefixed_name; + } + } + + return field_name; } FieldState FieldStore::getShapeDisp() const { return shape_disp_[0]; } @@ -139,10 +277,9 @@ const std::vector& FieldStore::getAllFields() const { return states_ std::vector FieldStore::getStates(const std::string& weak_form_name) const { // Validate that weak form is registered - SLIC_ERROR_ROOT_IF( - weak_form_name_to_field_names_.count(weak_form_name) == 0, - axom::fmt::format("Weak form '{}' not found in FieldStore. Did you forget to call addResireaction()?", - weak_form_name)); + SLIC_ERROR_ROOT_IF(weak_form_name_to_field_names_.count(weak_form_name) == 0, + axom::fmt::format("Weak form '{}' not found in FieldStore. Did you forget to call addReaction()?", + weak_form_name)); auto field_names = weak_form_name_to_field_names_.at(weak_form_name); std::vector fields_for_residual; @@ -166,10 +303,9 @@ std::vector FieldStore::getStatesFromVectors(const std::string& weak const std::vector& param_fields) const { // Validate that weak form is registered - SLIC_ERROR_ROOT_IF( - weak_form_name_to_field_names_.count(weak_form_name) == 0, - axom::fmt::format("Weak form '{}' not found in FieldStore. Did you forget to call addResireaction()?", - weak_form_name)); + SLIC_ERROR_ROOT_IF(weak_form_name_to_field_names_.count(weak_form_name) == 0, + axom::fmt::format("Weak form '{}' not found in FieldStore. Did you forget to call addReaction()?", + weak_form_name)); auto field_names = weak_form_name_to_field_names_.at(weak_form_name); std::vector fields_for_residual; @@ -199,6 +335,15 @@ std::vector FieldStore::getStatesFromVectors(const std::string& weak const std::shared_ptr& FieldStore::getMesh() const { return mesh_; } +std::shared_ptr FieldStore::getBoundaryConditions(const std::string& field_name) const +{ + auto it = boundary_conditions_.find(field_name); + if (it != boundary_conditions_.end()) { + return it->second; + } + return nullptr; +} + const std::shared_ptr& FieldStore::graph() const { return graph_; } const std::vector, FieldStore::TimeIntegrationMapping>>& @@ -207,18 +352,68 @@ FieldStore::getTimeIntegrationRules() const return time_integration_rules_; } -size_t FieldStore::getUnknownIndex(const std::string& field_name) const { return to_unknown_index_.at(field_name); } - void FieldStore::setField(size_t index, FieldState updated_field) { states_[index] = updated_field; } +void FieldStore::markWeakFormInternal(const std::string& weak_form_name) +{ + internal_weak_forms_.insert(weak_form_name); +} + void FieldStore::addWeakFormReaction(std::string weak_form_name, std::string field_name) { - weak_form_to_test_field_[weak_form_name] = field_name; + for (auto& kv : weak_form_to_test_field_) { + if (kv.first == weak_form_name) { + kv.second = field_name; + return; + } + } + weak_form_to_test_field_.push_back({weak_form_name, field_name}); } std::string FieldStore::getWeakFormReaction(const std::string& weak_form_name) const { - return weak_form_to_test_field_.at(weak_form_name); + for (const auto& kv : weak_form_to_test_field_) { + if (kv.first == weak_form_name) { + return kv.second; + } + } + SLIC_ERROR("Reaction field not found for weak form " << weak_form_name); + return ""; +} + +const std::vector& FieldStore::getParameterFields() const { return params_; } + +const std::vector& FieldStore::getStateFields() const { return states_; } + +std::vector FieldStore::getOutputFieldStates() const +{ + std::vector output; + std::set public_static_fields; + for (const auto& [rule, mapping] : time_integration_rules_) { + if (mapping.history_name.empty() && mapping.dot_name.empty() && mapping.ddot_name.empty()) { + public_static_fields.insert(mapping.primary_name); + } + } + for (size_t i = 0; i < states_.size(); ++i) { + if (!is_solve_state_[i] || public_static_fields.count(states_[i].get()->name()) > 0) { + output.push_back(states_[i]); + } + } + return output; +} + +std::vector FieldStore::getReactionInfos() const +{ + std::vector infos; + for (const auto& kv : weak_form_to_test_field_) { + const std::string& weak_form_name = kv.first; + if (internal_weak_forms_.count(weak_form_name)) { + continue; + } + const std::string& field_name = kv.second; + infos.push_back({weak_form_name, &getField(field_name).get()->space()}); + } + return infos; } } // namespace smith diff --git a/src/smith/differentiable_numerics/field_store.hpp b/src/smith/differentiable_numerics/field_store.hpp index fc594a6b55..d85b3a6338 100644 --- a/src/smith/differentiable_numerics/field_store.hpp +++ b/src/smith/differentiable_numerics/field_store.hpp @@ -8,10 +8,11 @@ #include "smith/differentiable_numerics/field_state.hpp" #include "smith/differentiable_numerics/time_integration_rule.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" #include "smith/physics/mesh.hpp" #include +#include #include #include #include @@ -22,20 +23,38 @@ class DirichletBoundaryConditions; class BoundaryConditionManager; /** - * @brief Representation of a field type with a name and an optional unknown index. + * @brief Information about a dual field. + */ +struct ReactionInfo { + std::string name; ///< The name of the dual field. + const mfem::ParFiniteElementSpace* space = nullptr; ///< The finite element space of the dual field. +}; + +/** + * @brief Representation of a field type with a name and a flag indicating whether it is an + * active Jacobian unknown in the current weak-form context. + * + * @c is_unknown is intentionally a per-instance flag, not a global property. The same field + * may be a Jacobian variable in one weak form (e.g. displacement in the main solid solve) and + * a fixed input in another (e.g. displacement in the stress projection). Callers control this + * by passing the same @c FieldType object (set by @c addIndependent) or a plain copy with the + * default @c is_unknown = false. + * * @tparam Space The finite element space type. * @tparam Time The time integration type (unused by default). */ template struct FieldType { + using space_type = Space; ///< The finite element space type. + /** * @brief Construct a new FieldType object. * @param n Name of the field. - * @param unknown_index_ Index of the unknown in the solver (default: -1). + * @param is_unknown_ Whether this field is a Jacobian unknown in the current context. */ - FieldType(std::string n, int unknown_index_ = -1) : name(n), unknown_index(unknown_index_) {} - std::string name; ///< Name of the field. - int unknown_index; ///< Index of the unknown in the solver. + FieldType(std::string n, bool is_unknown_ = false) : name(n), is_unknown(is_unknown_) {} + std::string name; ///< Name of the field. + bool is_unknown = false; ///< True if this field is a Jacobian variable in the current weak-form context. }; /** @@ -46,8 +65,18 @@ struct FieldStore { * @brief Construct a new FieldStore object. * @param mesh The mesh associated with the fields. * @param storage_size Initial storage size for fields (default: 50). + * @param prepend_name Namespace prefix applied by @c prefix(). Empty means no prefix. */ - FieldStore(std::shared_ptr mesh, size_t storage_size = 50); + FieldStore(std::shared_ptr mesh, size_t storage_size = 50, std::string prepend_name = ""); + + /** + * @brief Apply this store's namespace prefix to a base name. + * + * Returns @p base unchanged when the store was constructed with an empty prepend name, + * otherwise returns @c prepend_name_ + "_" + base. Factories use this to namespace + * weak form, field, and parameter names consistently without re-implementing the rule. + */ + std::string prefix(const std::string& base) const; /** * @brief Enum for different types of time derivatives. @@ -66,8 +95,9 @@ struct FieldStore { * @param type The field type specification. */ template - void addShapeDisp(FieldType type) + void addShapeDisp(FieldType& type) { + type.name = prefix(type.name); shape_disp_.push_back(smith::createFieldState(*graph_, Space{}, type.name, mesh_->tag())); } @@ -77,8 +107,19 @@ struct FieldStore { * @param type The field type specification. */ template - void addParameter(FieldType type) + void addParameter(FieldType& type) { + type.name = prefix(type.name); + if (to_params_index_.count(type.name)) { + // Already registered — expected when multiple systems share the same parameter. + // Verify the space matches by checking vdim (== Space::components). + auto& existing = params_[to_params_index_.at(type.name)]; + SLIC_ERROR_ROOT_IF(existing.get()->space().GetVDim() != Space::components, + axom::fmt::format("Parameter '{}' re-registered with a different space " + "(existing vdim={}, new vdim={})", + type.name, existing.get()->space().GetVDim(), Space::components)); + return; + } to_params_index_[type.name] = params_.size(); params_.push_back(smith::createFieldState(*graph_, Space{}, type.name, mesh_->tag())); } @@ -86,14 +127,13 @@ struct FieldStore { /** * @brief Add an independent field (a solver unknown) to the store. * - * Registers the field as an unknown and assigns it an index in the solver's block structure. - * The @p type argument is mutated in-place: its @c unknown_index is set to the assigned index, - * so the same @c FieldType object can later be passed to @c createSpaces to tell the - * weak form that this argument is an active unknown (i.e. the Jacobian should be computed - * with respect to it). + * Registers the field as an unknown by setting @c type.is_unknown = true, so the same + * @c FieldType object can later be passed to @c createSpaces to mark this argument + * as an active Jacobian variable. Also creates a boundary-condition slot keyed by field + * name that callers can populate after this call returns. * * @tparam Space The finite element space type. - * @param type The field type specification; @c type.unknown_index is set on return. + * @param type The field type specification; @c type.is_unknown is set to @c true on return. * @param time_rule The time integration rule governing how this unknown and its dependents * are related across time steps. * @return std::shared_ptr The boundary conditions for this field. @@ -102,15 +142,14 @@ struct FieldStore { std::shared_ptr addIndependent(FieldType& type, std::shared_ptr time_rule) { - type.unknown_index = static_cast(num_unknowns_); + type.name = prefix(type.name); + type.is_unknown = true; to_states_index_[type.name] = states_.size(); - to_unknown_index_[type.name] = num_unknowns_; FieldState new_field = smith::createFieldState(*graph_, Space{}, type.name, mesh_->tag()); states_.push_back(new_field); + is_solve_state_.push_back(true); auto latest_bc = addBoundaryConditions(new_field.get()); - ++num_unknowns_; - SLIC_ERROR_IF(num_unknowns_ != boundary_conditions_.size(), - "Inconcistency between num unknowns and boundary condition size"); + boundary_conditions_[type.name] = latest_bc; SLIC_ERROR_IF(!time_rule, "Invalid time_rule"); @@ -132,23 +171,17 @@ struct FieldStore { * (predicted_value, stored_old_value). * * **Return value:** a @c FieldType whose @c name is the name of the newly registered - * field. This object is intentionally returned (rather than discarded) so that callers can - * pass it directly to @c createSpaces when assembling the weak-form argument list. The - * position at which a @c FieldType appears in that argument list determines which slot of the - * lambda the field occupies at quadrature-point evaluation time, which is how the time - * integration rules reconstruct quantities such as - * @f$ \dot{\alpha} = (\alpha_\text{predicted} - \alpha_\text{old}) / \Delta t @f$. - * - * The field name is derived automatically from the independent field's name plus a suffix that - * reflects the derivative level (@c _old for VALUE, @c _dot_old for DOT, - * @c _ddot_old for DDOT), unless @p name_override is supplied. + * field and @c is_unknown is @c false. This object is intentionally returned (rather than + * discarded) so that callers can pass it directly to @c createSpaces when assembling the + * weak-form argument list. To make it the Jacobian variable for a specific weak form (e.g. + * the cycle-zero acceleration solve), copy the returned object and set @c is_unknown = true + * before passing to @c createSpaces. * * @tparam Space The finite element space type (must match the independent field). * @param independent_field The @c FieldType of the independent (predicted) field. * @param derivative Which time-derivative level this history field stores. * @param name_override If non-empty, use this as the field name instead of the auto-generated one. - * @return FieldType Type descriptor for the newly created dependent field; pass this to - * @c createSpaces to register it as a weak-form argument. + * @return FieldType Type descriptor for the newly created dependent field. */ template auto addDependent(FieldType independent_field, TimeDerivative derivative, std::string name_override = "") @@ -164,7 +197,7 @@ struct FieldStore { SLIC_ERROR("Unsupported TimeDerivative"); } - std::string name = name_override.empty() ? independent_field.name + suffix : name_override; + std::string name = name_override.empty() ? independent_field.name + suffix : prefix(name_override); if (independent_name_to_rule_index_.count(independent_field.name)) { size_t rule_idx = independent_name_to_rule_index_.at(independent_field.name); @@ -183,6 +216,7 @@ struct FieldStore { to_states_index_[name] = states_.size(); states_.push_back(smith::createFieldState(*graph_, Space{}, name, mesh_->tag())); + is_solve_state_.push_back(false); return FieldType(name); } @@ -202,12 +236,6 @@ struct FieldStore { */ void addWeakFormArg(std::string weak_form_name, std::string argument_name, size_t argument_index); - /** - * @brief Get the number of unknowns in the field store. - * @return size_t Number of unknowns. - */ - size_t getNumUnknowns() const { return num_unknowns_; } - /** * @brief Register the reaction (test) field for a weak form. * @@ -220,6 +248,14 @@ struct FieldStore { */ void addWeakFormReaction(std::string weak_form_name, std::string field_name); + /** + * @brief Mark a weak form as internal so it is excluded from getReactionInfos(). + * + * Use this for subsystem forms (e.g. cycle-zero acceleration solve) that should not be + * exposed as user-visible reactions in DifferentiablePhysics. + */ + void markWeakFormInternal(const std::string& weak_form_name); + /** * @brief Get the name of the reaction (test) field for a weak form. * @param weak_form_name Name of the weak form. @@ -233,9 +269,17 @@ struct FieldStore { * This is the primary setup method for constructing a weak form. It: * 1. Registers @p reaction_field_name as the reaction/test field via @c addWeakFormReaction. * 2. Iterates over every @c FieldType in @p types (in order), registering each as an input - * argument to the weak form and recording whether it is an active unknown. - * 3. Returns the ordered vector of finite element spaces, which can be passed directly to - * the @c TimeDiscretizedWeakForm constructor without creating a named temporary. + * argument. If @c type.is_unknown is @c true, the field is also registered as an active + * Jacobian unknown for this weak form. + * 3. Returns the ordered vector of finite element spaces. + * + * A field may have @c is_unknown = true in one weak form and @c false in another (e.g. + * displacement is a Jacobian variable in the main solid solve but a fixed input in the stress + * projection). Callers control this by passing the @c FieldType returned from @c addIndependent + * (has @c is_unknown = true) or a plain copy constructed from the field name (has @c is_unknown + * = false). Similarly, a dependent field can be made the Jacobian variable for a specific weak + * form (e.g. acceleration in the cycle-zero solve) by copying the returned @c FieldType and + * setting @c is_unknown = true. * * @param weak_form_name Name of the weak form being constructed. * @param reaction_field_name Name of the test/reaction field (may differ from the first input). @@ -253,7 +297,7 @@ struct FieldStore { auto register_field = [&](auto type) { spaces.push_back(&getField(type.name).get()->space()); addWeakFormArg(weak_form_name, type.name, arg_num); - if (type.unknown_index >= 0) { + if (type.is_unknown) { addWeakFormUnknownArg(weak_form_name, type.name, arg_num); } ++arg_num; @@ -293,49 +337,58 @@ struct FieldStore { std::vector> indexMap(const std::vector& residual_names) const; /** - * @brief Get the boundary condition managers for all independent fields. - * @return std::vector List of boundary condition managers. + * @brief Get the boundary condition managers for the given weak forms, one per residual row. + * + * For each weak form in @p weak_form_names the reaction (test) field name is looked up. The + * returned manager is selected by consulting the registered @c TimeIntegrationMapping s: + * - reaction = primary or history slot -> value-level BC manager + * - reaction = second-derivative (ddot) slot -> second-derivative BC manager + * - reaction has its own DBC entry not tied to a mapping -> that DBC's value manager + * - otherwise -> @c nullptr (solver skips null entries) + * + * The second-derivative manager is rebuilt on each call, so late value-BC additions are reflected. + * + * @param weak_form_names Ordered list of weak form names whose BCs are needed. + * @return std::vector One entry per weak form, in order. */ - std::vector getBoundaryConditionManagers() const; + std::vector getBoundaryConditionManagers( + const std::vector& weak_form_names) const; + + /// @brief Get ordered boundary condition managers corresponding to an ordered list of fields. + std::vector getBoundaryConditionManagersForFields( + const std::vector& field_names) const; /** - * @brief Get the Dirichlet boundary conditions for an independent field by its unknown index. - * @param unknown_index The unknown index of the independent field. - * @return std::shared_ptr The boundary conditions. + * @brief Check whether a field exists. + * + * Accepts either a fully-qualified field name or an unprefixed base name. */ - std::shared_ptr getBoundaryConditions(size_t unknown_index) const; + bool hasField(const std::string& field_name) const; /** * @brief Get the internal index of a field by name. - * @param field_name Name of the field. + * @param field_name Fully-qualified or unprefixed field name. * @return size_t Index of the field. */ size_t getFieldIndex(const std::string& field_name) const; - /** - * @brief Get the unknown index of a field by name. - * @param field_name Name of the field. - * @return size_t Unknown index of the field. - */ - size_t getUnknownIndex(const std::string& field_name) const; - /** * @brief Get a FieldState by name. - * @param field_name Name of the field. + * @param field_name Fully-qualified or unprefixed field name. * @return FieldState The field state. */ FieldState getField(const std::string& field_name) const; /** * @brief Get a parameter field by name. - * @param param_name Name of the parameter. + * @param param_name Fully-qualified or unprefixed parameter name. * @return FieldState The parameter field state. */ FieldState getParameter(const std::string& param_name) const; /** * @brief Update a field in the store by name. - * @param field_name Name of the field. + * @param field_name Fully-qualified or unprefixed field name. * @param updated_field The new field state. */ void setField(const std::string& field_name, FieldState updated_field); @@ -378,11 +431,35 @@ struct FieldStore { const std::vector& param_fields) const; /** - * @brief Get the associated mesh. - * @return const std::shared_ptr& The mesh. + * @brief Get the list of all parameter fields. + */ + const std::vector& getParameterFields() const; + + /** + * @brief Get the list of all state fields. + */ + const std::vector& getStateFields() const; + + /** + * @brief Get the list of physical, non-solve state fields suitable for output. + */ + std::vector getOutputFieldStates() const; + + /** + * @brief Get information about reaction fields. + */ + std::vector getReactionInfos() const; + + /** + * @brief Get associated mesh shared by all registered fields. */ const std::shared_ptr& getMesh() const; + /** + * @brief Get the boundary conditions for a given field name. + */ + std::shared_ptr getBoundaryConditions(const std::string& field_name) const; + /** * @brief Get the associated data store graph. * @return const std::shared_ptr& The graph. @@ -390,19 +467,22 @@ struct FieldStore { const std::shared_ptr& graph() const; private: + std::string resolveFieldName(const std::string& field_name) const; + std::shared_ptr mesh_; std::shared_ptr graph_; + std::string prepend_name_; std::vector shape_disp_; std::vector params_; std::vector states_; + std::vector is_solve_state_; std::map to_states_index_; std::map to_params_index_; - size_t num_unknowns_ = 0; - std::map to_unknown_index_; - std::vector> boundary_conditions_; + /// Boundary conditions keyed by primary unknown field name. + std::map> boundary_conditions_; struct FieldLabel { std::string field_name; @@ -416,14 +496,15 @@ struct FieldStore { std::map> weak_form_name_to_field_indices_; std::map> weak_form_name_to_field_names_; - std::map weak_form_to_test_field_; + std::vector> weak_form_to_test_field_; + std::set internal_weak_forms_; ///< weak forms excluded from getReactionInfos() (subsystem-internal) std::vector, TimeIntegrationMapping>> time_integration_rules_; std::map independent_name_to_rule_index_; }; /** - * @brief Create a TimeDiscretizedWeakForm and register its fields in the FieldStore. + * @brief Create a FunctionalWeakForm and register its fields in the FieldStore. * * Thin convenience wrapper: registers @p test_type as the reaction field, registers all * @p field_types as input arguments, and constructs the weak form in one call. @@ -432,9 +513,19 @@ template auto createWeakForm(std::string name, FieldType test_type, FieldStore& field_store, FieldType... field_types) { - return std::make_shared>>( + return std::make_shared>>( name, field_store.getMesh(), field_store.getField(test_type.name).get()->space(), field_store.createSpaces(name, test_type.name, field_types...)); } +/** + * @brief Construct a `shared_ptr` over a mesh. Inline call-site helper for + * standalone-physics setup; equivalent to `std::make_shared(mesh, ...)`. + */ +inline std::shared_ptr fieldStore(std::shared_ptr mesh, std::size_t storage_size = 200, + std::string prefix = "") +{ + return std::make_shared(std::move(mesh), storage_size, std::move(prefix)); +} + } // namespace smith diff --git a/src/smith/differentiable_numerics/lumped_mass_weak_form.hpp b/src/smith/differentiable_numerics/lumped_mass_weak_form.hpp index 5799d53e2b..c3c1f0ef1c 100644 --- a/src/smith/differentiable_numerics/lumped_mass_weak_form.hpp +++ b/src/smith/differentiable_numerics/lumped_mass_weak_form.hpp @@ -27,7 +27,7 @@ auto createSolidMassWeakForm(const std::string& physics_name, std::shared_ptr>; auto weak_form = std::make_shared(physics_name, mesh, lumped_field.space(), typename WeakFormT::SpacesT{&density.space()}); - weak_form->addBodyIntegral(smith::DependsOn<0>{}, mesh->entireBodyName(), [](double /*time*/, auto /*X*/, auto Rho) { + weak_form->addBodyIntegral(mesh->entireBodyName(), [](auto /*t_info*/, auto /*X*/, auto Rho) { if constexpr (lumped_dim == 1) { return smith::tuple{get(Rho), tensor{}}; } else { diff --git a/src/smith/differentiable_numerics/make_time_info_material.hpp b/src/smith/differentiable_numerics/make_time_info_material.hpp new file mode 100644 index 0000000000..f442dd6c68 --- /dev/null +++ b/src/smith/differentiable_numerics/make_time_info_material.hpp @@ -0,0 +1,42 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#pragma once + +#include + +#include "smith/physics/common.hpp" +#include "smith/infrastructure/accelerator.hpp" + +namespace smith { + +/// @brief Adapter that lets a material without time information satisfy the TimeInfo material interface. +template +struct TimeInfoMaterial { + /// State type forwarded from the wrapped material. + using State = typename Material::State; + + Material material; ///< Wrapped material. + double density; ///< Density forwarded for solid mechanics inertial terms. + + /// @brief Evaluate the wrapped material, ignoring TimeInfo and velocity gradient. + template + SMITH_HOST_DEVICE auto operator()(const TimeInfo& /*t_info*/, StateType&& state, GradUType&& grad_u, + const GradVType& /*grad_v*/, Args&&... args) const + { + return material(std::forward(state), std::forward(grad_u), std::forward(args)...); + } +}; + +/// @brief Create a TimeInfoMaterial adapter for a material with signature material(state, grad_u, args...). +template +TimeInfoMaterial makeTimeInfoMaterial(Material mat) +{ + const double density = mat.density; + return TimeInfoMaterial{std::move(mat), density}; +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/multiphysics_time_integrator.cpp b/src/smith/differentiable_numerics/multiphysics_time_integrator.cpp index dd5c0e0556..7979726a51 100644 --- a/src/smith/differentiable_numerics/multiphysics_time_integrator.cpp +++ b/src/smith/differentiable_numerics/multiphysics_time_integrator.cpp @@ -7,7 +7,7 @@ #include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" #include "smith/differentiable_numerics/nonlinear_solve.hpp" -#include "smith/differentiable_numerics/coupled_system_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" #include "smith/differentiable_numerics/reaction.hpp" @@ -16,17 +16,23 @@ namespace smith { -MultiphysicsTimeIntegrator::MultiphysicsTimeIntegrator(std::shared_ptr field_store, - const std::vector>& weak_forms, - std::shared_ptr solver, - std::shared_ptr cycle_zero_weak_form, - std::shared_ptr cycle_zero_solver) - : field_store_(field_store), - weak_forms_(weak_forms), - solver_(solver), - cycle_zero_weak_form_(cycle_zero_weak_form), - cycle_zero_solver_(cycle_zero_solver) +MultiphysicsTimeIntegrator::MultiphysicsTimeIntegrator(std::shared_ptr system, + std::vector> cycle_zero_systems, + std::vector> post_solve_systems) + : system_(system), + cycle_zero_systems_(std::move(cycle_zero_systems)), + post_solve_systems_(std::move(post_solve_systems)) { + for (size_t i = 0; i < system_->weak_forms.size(); ++i) { + const std::string wf_name = system_->weak_forms[i]->name(); + const std::string reaction_name = system_->field_store->getWeakFormReaction(wf_name); + main_unknown_name_to_local_idx_[reaction_name] = i; + } +} + +void MultiphysicsTimeIntegrator::addPostSolveSystem(std::shared_ptr system) +{ + post_solve_systems_.push_back(std::move(system)); } std::pair, std::vector> MultiphysicsTimeIntegrator::advanceState( @@ -35,104 +41,105 @@ std::pair, std::vector> MultiphysicsTimeI { std::vector current_states = states; + // Sync FieldStore with (possibly updated) states and params so they are current for solve + system_->field_store->setField(system_->field_store->getShapeDisp().get()->name(), shape_disp); + + for (size_t i = 0; i < current_states.size(); ++i) { + system_->field_store->setField(i, current_states[i]); + } + // Optional: update parameter fields as well? (assuming they are aligned) + SLIC_ERROR_ROOT_IF(params.size() != system_->field_store->getParameterFields().size(), + "Parameter size mismatch in advanceState"); + for (size_t i = 0; i < params.size(); ++i) { + system_->field_store->setField(system_->field_store->getParameterFields()[i].get()->name(), params[i]); + } + // Handle initial acceleration solve at cycle 0 const bool requires_cycle_zero_solve = - std::any_of(field_store_->getTimeIntegrationRules().begin(), field_store_->getTimeIntegrationRules().end(), - [](const auto& rule_and_mapping) { + std::any_of(system_->field_store->getTimeIntegrationRules().begin(), + system_->field_store->getTimeIntegrationRules().end(), [](const auto& rule_and_mapping) { return rule_and_mapping.first && rule_and_mapping.first->requiresInitialAccelerationSolve(); }); - if (time_info.cycle() == 0 && cycle_zero_weak_form_ && requires_cycle_zero_solve) { - for (size_t i = 0; i < current_states.size(); ++i) { - field_store_->setField(i, current_states[i]); - } - - std::string test_field_name = field_store_->getWeakFormReaction(cycle_zero_weak_form_->name()); - std::vector wf_fields = field_store_->getStates(cycle_zero_weak_form_->name()); - FieldState test_field = field_store_->getField(test_field_name); - size_t test_field_idx_in_wf = invalid_block_index; - for (size_t j = 0; j < wf_fields.size(); ++j) { - if (wf_fields[j].get() == test_field.get()) { - test_field_idx_in_wf = j; - break; - } - } - SLIC_ERROR_IF(test_field_idx_in_wf == invalid_block_index, "Test field '" << test_field_name - << "' not found in cycle-zero weak form '" - << cycle_zero_weak_form_->name() << "'"); - - std::vector wf_ptrs = {cycle_zero_weak_form_.get()}; - std::vector> block_indices = {{test_field_idx_in_wf}}; - - std::vector bcs; - auto all_bcs = field_store_->getBoundaryConditionManagers(); - size_t test_field_unknown_idx = invalid_block_index; - try { - test_field_unknown_idx = field_store_->getUnknownIndex(test_field_name); - } catch (const std::out_of_range&) { - for (const auto& [rule, mapping] : field_store_->getTimeIntegrationRules()) { - static_cast(rule); - if (mapping.primary_name == test_field_name || mapping.history_name == test_field_name || - mapping.dot_name == test_field_name || mapping.ddot_name == test_field_name) { - test_field_unknown_idx = field_store_->getUnknownIndex(mapping.primary_name); - break; - } + if (time_info.cycle() == 0 && !cycle_zero_systems_.empty() && requires_cycle_zero_solve) { + for (const auto& cz_sys : cycle_zero_systems_) { + TimeInfo cycle_zero_time_info(time_info.time() - time_info.dt(), time_info.dt(), time_info.cycle(), + TimeInfo::EvaluationMode::CycleZero); + auto cycle_zero_unknowns = cz_sys->solve(cycle_zero_time_info); + + SLIC_ERROR_ROOT_IF(cycle_zero_unknowns.size() != cz_sys->weak_forms.size(), + "Cycle zero system result count does not match number of cycle-zero weak forms"); + SLIC_ERROR_ROOT_IF(!cz_sys->solve_result_field_names.empty() && + cz_sys->solve_result_field_names.size() != cz_sys->weak_forms.size(), + "Cycle zero solve_result_field_names size does not match number of weak forms"); + for (size_t i = 0; i < cz_sys->weak_forms.size(); ++i) { + const std::string result_field_name = + cz_sys->solve_result_field_names.empty() + ? system_->field_store->getWeakFormReaction(cz_sys->weak_forms[i]->name()) + : cz_sys->solve_result_field_names[i]; + size_t result_field_state_idx = system_->field_store->getFieldIndex(result_field_name); + current_states[result_field_state_idx] = cycle_zero_unknowns[i]; + system_->field_store->setField(result_field_state_idx, cycle_zero_unknowns[i]); } } - SLIC_ERROR_IF(test_field_unknown_idx == invalid_block_index, - "Could not map cycle-zero test field '" << test_field_name << "' to an independent unknown."); - SLIC_ERROR_IF(test_field_unknown_idx >= all_bcs.size(), - "Cycle-zero test field '" << test_field_name << "' has unknown index " << test_field_unknown_idx - << ", but only " << all_bcs.size() << " BC managers are registered."); - bcs.push_back(all_bcs[test_field_unknown_idx]); - - std::vector> states_vec = {wf_fields}; - std::vector> params_vec = {params}; - - auto& cz_solver = cycle_zero_solver_ ? cycle_zero_solver_ : solver_; - auto result = cz_solver->solve(wf_ptrs, block_indices, shape_disp, states_vec, params_vec, time_info, bcs); - - size_t test_field_state_idx = field_store_->getFieldIndex(test_field_name); - current_states[test_field_state_idx] = result[0]; } - // Sync FieldStore with (possibly updated) states - for (size_t i = 0; i < current_states.size(); ++i) { - field_store_->setField(i, current_states[i]); - } + std::vector primary_unknowns = system_->solve(time_info); - std::vector primary_unknowns = solve(weak_forms_, *field_store_, solver_.get(), time_info, params); + // Build a map from the main system's unknown names to their position in primary_unknowns. + // Entries in the shared FieldStore's time integration rules that belong to post-solve + // subsystems (e.g. stress projection) are NOT present here and must be skipped by downstream + // lookups that walk getTimeIntegrationRules(). // Create states for reaction computation: newly solved primary unknowns + current states std::vector states_for_reactions = current_states; - for (const auto& [rule, mapping] : field_store_->getTimeIntegrationRules()) { - size_t u_idx = field_store_->getFieldIndex(mapping.primary_name); - size_t unknown_idx = field_store_->getUnknownIndex(mapping.primary_name); - FieldState u_new = primary_unknowns[unknown_idx]; + for (const auto& [rule, mapping] : system_->field_store->getTimeIntegrationRules()) { + auto it = main_unknown_name_to_local_idx_.find(mapping.primary_name); + if (it == main_unknown_name_to_local_idx_.end()) { + continue; // rule belongs to a post-solve subsystem, not the main solve + } + size_t u_idx = system_->field_store->getFieldIndex(mapping.primary_name); + FieldState u_new = primary_unknowns[it->second]; states_for_reactions[u_idx] = u_new; } // Compute reactions using newly solved unknowns but BEFORE time integration state updates - std::vector reactions; - for (const auto& wf : weak_forms_) { - std::vector wf_fields = field_store_->getStatesFromVectors(wf->name(), states_for_reactions, params); - std::string test_field_name = field_store_->getWeakFormReaction(wf->name()); - size_t test_field_idx = field_store_->getFieldIndex(test_field_name); - FieldState test_field = states_for_reactions[test_field_idx]; - reactions.push_back(smith::evaluateWeakForm(wf, time_info, shape_disp, wf_fields, test_field)); + std::vector reactions = system_->computeReactions(time_info, states_for_reactions); + + // Sync field_store with newly solved primary unknowns so post-solve systems (e.g. stress + // projection) read the current displacement rather than the pre-solve snapshot. + for (const auto& [rule, mapping] : system_->field_store->getTimeIntegrationRules()) { + auto it = main_unknown_name_to_local_idx_.find(mapping.primary_name); + if (it == main_unknown_name_to_local_idx_.end()) { + continue; + } + size_t u_idx = system_->field_store->getFieldIndex(mapping.primary_name); + system_->field_store->setField(u_idx, primary_unknowns[it->second]); } - // Now do time integration to compute corrected velocities/accelerations and update all states - const auto& all_current_states = field_store_->getAllFields(); - std::vector new_states = current_states; - for (size_t i = 0; i < current_states.size(); ++i) { - new_states[i] = all_current_states[i]; + // Solve post-solve systems (e.g. stress projection for output) and sync their results back + // into the shared field_store so getAllFields() returns the updated values for new_states. + for (const auto& ps : post_solve_systems_) { + auto ps_unknowns = ps->solve(time_info); + for (size_t i = 0; i < ps->weak_forms.size(); ++i) { + const std::string reaction_name = ps->field_store->getWeakFormReaction(ps->weak_forms[i]->name()); + size_t u_idx = ps->field_store->getFieldIndex(reaction_name); + ps->field_store->setField(u_idx, ps_unknowns[i]); + } } - for (const auto& [rule, mapping] : field_store_->getTimeIntegrationRules()) { - size_t u_idx = field_store_->getFieldIndex(mapping.primary_name); - size_t unknown_idx = field_store_->getUnknownIndex(mapping.primary_name); - FieldState u_new = primary_unknowns[unknown_idx]; + // Now do time integration to compute corrected velocities/accelerations and update all states. + // Seed new_states from field_store, which already reflects post-solve subsystem updates and + // the freshly synced primary unknowns. + std::vector new_states = system_->field_store->getAllFields(); + + for (const auto& [rule, mapping] : system_->field_store->getTimeIntegrationRules()) { + auto it = main_unknown_name_to_local_idx_.find(mapping.primary_name); + if (it == main_unknown_name_to_local_idx_.end()) { + continue; // rule belongs to a post-solve subsystem, not the main solve + } + size_t u_idx = system_->field_store->getFieldIndex(mapping.primary_name); + FieldState u_new = primary_unknowns[it->second]; new_states[u_idx] = u_new; std::vector rule_inputs; @@ -142,58 +149,50 @@ std::pair, std::vector> MultiphysicsTimeI } if (rule->num_args() >= 3 && !mapping.dot_name.empty()) { - size_t v_idx = field_store_->getFieldIndex(mapping.dot_name); + size_t v_idx = system_->field_store->getFieldIndex(mapping.dot_name); rule_inputs.push_back(current_states[v_idx]); } if (rule->num_args() >= 4 && !mapping.ddot_name.empty()) { - size_t a_idx = field_store_->getFieldIndex(mapping.ddot_name); + size_t a_idx = system_->field_store->getFieldIndex(mapping.ddot_name); rule_inputs.push_back(current_states[a_idx]); } if (!mapping.dot_name.empty()) { - size_t v_idx = field_store_->getFieldIndex(mapping.dot_name); + size_t v_idx = system_->field_store->getFieldIndex(mapping.dot_name); new_states[v_idx] = rule->corrected_dot(time_info, rule_inputs); } if (!mapping.ddot_name.empty()) { - size_t a_idx = field_store_->getFieldIndex(mapping.ddot_name); + size_t a_idx = system_->field_store->getFieldIndex(mapping.ddot_name); new_states[a_idx] = rule->corrected_ddot(time_info, rule_inputs); } if (!mapping.history_name.empty()) { - size_t hist_idx = field_store_->getFieldIndex(mapping.history_name); + size_t hist_idx = system_->field_store->getFieldIndex(mapping.history_name); new_states[hist_idx] = u_new; } } - return {new_states, reactions}; -} - -std::vector solve(const std::vector>& weak_forms, const FieldStore& field_store, - const CoupledSystemSolver* solver, const TimeInfo& time_info, - const std::vector& params) -{ - std::vector weak_form_names; - for (const auto& wf : weak_forms) { - weak_form_names.push_back(wf->name()); + // Copy solve-state → history for post-solve fields when a public history field exists. + // The main loop skipped these rules; their primary fields are already correct in new_states + // (populated from all_current_states above), so only the history field needs updating. + for (const auto& [rule, mapping] : system_->field_store->getTimeIntegrationRules()) { + if (main_unknown_name_to_local_idx_.count(mapping.primary_name)) { + continue; // already handled by main time integration loop above + } + if (!mapping.history_name.empty()) { + size_t primary_idx = system_->field_store->getFieldIndex(mapping.primary_name); + size_t hist_idx = system_->field_store->getFieldIndex(mapping.history_name); + new_states[hist_idx] = new_states[primary_idx]; + } } - std::vector> index_map = field_store.indexMap(weak_form_names); - std::vector> inputs; - for (size_t i = 0; i < weak_forms.size(); ++i) { - std::string wf_name = weak_forms[i]->name(); - std::vector fields_for_wk = field_store.getStates(wf_name); - inputs.push_back(fields_for_wk); + for (size_t i = 0; i < new_states.size(); ++i) { + system_->field_store->setField(i, new_states[i]); } - std::vector> wk_params(weak_forms.size(), params); - std::vector weak_form_ptrs; - for (auto& p : weak_forms) { - weak_form_ptrs.push_back(p.get()); - } - return solver->solve(weak_form_ptrs, index_map, field_store.getShapeDisp(), inputs, wk_params, time_info, - field_store.getBoundaryConditionManagers()); + return {new_states, reactions}; } } // namespace smith diff --git a/src/smith/differentiable_numerics/multiphysics_time_integrator.hpp b/src/smith/differentiable_numerics/multiphysics_time_integrator.hpp index 71ce34c2c3..e92e5519e9 100644 --- a/src/smith/differentiable_numerics/multiphysics_time_integrator.hpp +++ b/src/smith/differentiable_numerics/multiphysics_time_integrator.hpp @@ -10,49 +10,35 @@ #include "smith/differentiable_numerics/field_state.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" #include "smith/numerics/solver_config.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" #include "smith/differentiable_numerics/time_integration_rule.hpp" #include "smith/physics/mesh.hpp" #include "smith/differentiable_numerics/field_store.hpp" +#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" namespace smith { -class CoupledSystemSolver; +class SystemSolver; class DirichletBoundaryConditions; class BoundaryConditionManager; -/** - * @brief Solve a set of weak forms. - * @param weak_forms List of weak forms to solve. - * @param field_store Field store containing the fields. - * @param solver The solver to use. - * @param time_info Current time information. - * @param params Optional parameter fields. - * @return std::vector The updated state fields. - */ -std::vector solve(const std::vector>& weak_forms, const FieldStore& field_store, - const CoupledSystemSolver* solver, const TimeInfo& time_info, - const std::vector& params = {}); - /** * @brief Time integrator for multiphysics problems, coordinating multiple weak forms. */ class MultiphysicsTimeIntegrator : public StateAdvancer { public: /** - * @brief Construct a new MultiphysicsTimeIntegrator object. - * @param field_store Field store containing the fields. - * @param weak_forms List of weak forms to coordinate. - * @param solver The block solver to use. - * @param cycle_zero_weak_form Optional weak form for initial acceleration solve at cycle 0. - * @param cycle_zero_solver Optional solver paired with `cycle_zero_weak_form` for the cycle-0 solve. - * If null, the integrator falls back to the main solver supplied here. + * @brief Construct a multiphysics advancer around main and auxiliary systems. + * @param system Main system solved every timestep. + * @param cycle_zero_systems Optional startup systems solved independently before first regular step. + * @param post_solve_systems Optional systems solved after the main step. */ - MultiphysicsTimeIntegrator(std::shared_ptr field_store, - const std::vector>& weak_forms, - std::shared_ptr solver, - std::shared_ptr cycle_zero_weak_form = nullptr, - std::shared_ptr cycle_zero_solver = nullptr); + MultiphysicsTimeIntegrator(std::shared_ptr system, + std::vector> cycle_zero_systems = {}, + std::vector> post_solve_systems = {}); + + /// @brief Register a system to be solved after the main solve and reaction computation. + void addPostSolveSystem(std::shared_ptr system); /** * @brief Advance the multiphysics state by one time step. @@ -67,11 +53,20 @@ class MultiphysicsTimeIntegrator : public StateAdvancer { const std::vector& params) const override; private: - std::shared_ptr field_store_; - std::vector> weak_forms_; - std::shared_ptr solver_; - std::shared_ptr cycle_zero_weak_form_; - std::shared_ptr cycle_zero_solver_; + std::shared_ptr system_; + std::vector> cycle_zero_systems_; + std::vector> post_solve_systems_; + + std::map main_unknown_name_to_local_idx_; }; +/// @brief Build a `MultiphysicsTimeIntegrator` using the system's own auxiliary systems. +inline std::shared_ptr makeAdvancer(std::shared_ptr system) +{ + auto cycle_zero_systems = system->cycle_zero_systems; + auto post_solve_systems = system->post_solve_systems; + return std::make_shared(std::move(system), std::move(cycle_zero_systems), + std::move(post_solve_systems)); +} + } // namespace smith diff --git a/src/smith/differentiable_numerics/nonlinear_block_solver.cpp b/src/smith/differentiable_numerics/nonlinear_block_solver.cpp index c5ac92d3b6..ae0bfe83f3 100644 --- a/src/smith/differentiable_numerics/nonlinear_block_solver.cpp +++ b/src/smith/differentiable_numerics/nonlinear_block_solver.cpp @@ -68,28 +68,16 @@ std::shared_ptr NonlinearBlockSolver::cloneFresh() const nonlinear_opts.relative_tol, nonlinear_opts, linear_opts); } -ConvergenceStatus NonlinearBlockSolver::convergenceStatus(double tolerance_multiplier, - const std::vector& residuals, - NonlinearConvergenceContext& context) const -{ - auto block_norms = computeResidualBlockNorms(residuals, comm_); - return evaluateResidualConvergence(tolerance_multiplier, abs_tol_, rel_tol_, block_norms, context); -} - -void NonlinearBlockSolver::primeConvergenceContext(const std::vector& residuals, - NonlinearConvergenceContext& context) const -{ - static_cast(convergenceStatus(1.0, residuals, context)); -} - void NonlinearBlockSolver::completeSetup(const std::vector& us) const { if (us.empty() || !nonlinear_solver_) return; if (!retained_linear_options_ || retained_linear_options_->preconditioner == Preconditioner::None) return; // Pick the field with the highest vdim (typically the displacement field for vector problems). + SLIC_ERROR_IF(!us[0], "NonlinearBlockSolver::completeSetup received a null field"); const FieldT* best = us[0].get(); for (const auto& u : us) { + SLIC_ERROR_IF(!u, "NonlinearBlockSolver::completeSetup received a null field"); if (u->space().GetVDim() > best->space().GetVDim()) best = u.get(); } @@ -111,6 +99,20 @@ void NonlinearBlockSolver::completeSetup(const std::vector& us) const #endif } +ConvergenceStatus NonlinearBlockSolver::convergenceStatus(double tolerance_multiplier, + const std::vector& residuals, + NonlinearConvergenceContext& context) const +{ + auto block_norms = computeResidualBlockNorms(residuals, comm_); + return evaluateResidualConvergence(tolerance_multiplier, abs_tol_, rel_tol_, block_norms, context); +} + +void NonlinearBlockSolver::primeConvergenceContext(const std::vector& residuals, + NonlinearConvergenceContext& context) const +{ + static_cast(convergenceStatus(1.0, residuals, context)); +} + std::vector NonlinearBlockSolver::solve( const std::vector& u_guesses, std::function(const std::vector&)> residual_funcs, @@ -246,7 +248,10 @@ std::vector NonlinearBlockSolver::solveAdjoi auto block_jac = std::make_unique(block_offsets); for (int i = 0; i < num_rows; ++i) { for (int j = 0; j < num_rows; ++j) { - block_jac->SetBlock(i, j, jacobian_transposed[static_cast(i)][static_cast(j)].get()); + auto& J = jacobian_transposed[static_cast(i)][static_cast(j)]; + if (J) { + block_jac->SetBlock(i, j, J.get()); + } } } diff --git a/src/smith/differentiable_numerics/nonlinear_solve.cpp b/src/smith/differentiable_numerics/nonlinear_solve.cpp index 63ca060739..e600f6cec6 100644 --- a/src/smith/differentiable_numerics/nonlinear_solve.cpp +++ b/src/smith/differentiable_numerics/nonlinear_solve.cpp @@ -250,7 +250,9 @@ std::vector block_solve(const std::vector& residual_evals for (size_t row_i = 0; row_i < num_rows; ++row_i) { for (size_t col_j = 0; col_j < num_rows; ++col_j) { - input_fields[row_i][block_indices[row_i][col_j]] = diagonal_fields[col_j]; + if (block_indices[row_i][col_j] != invalid_block_index) { + input_fields[row_i][block_indices[row_i][col_j]] = diagonal_fields[col_j]; + } } } @@ -349,7 +351,9 @@ std::vector block_solve(const std::vector& residual_evals // No sensitivity needed for primal fields for (size_t row_i = 0; row_i < num_rows; ++row_i) { for (size_t col_j = 0; col_j < num_rows; ++col_j) { - field_sensitivities[row_i][block_indices[row_i][col_j]] = nullptr; + if (block_indices[row_i][col_j] != invalid_block_index) { + field_sensitivities[row_i][block_indices[row_i][col_j]] = nullptr; + } } } diff --git a/src/smith/differentiable_numerics/solid_mechanics_system.hpp b/src/smith/differentiable_numerics/solid_mechanics_system.hpp index 2eb9bf7208..cc3a885984 100644 --- a/src/smith/differentiable_numerics/solid_mechanics_system.hpp +++ b/src/smith/differentiable_numerics/solid_mechanics_system.hpp @@ -11,16 +11,19 @@ #pragma once +#include + #include "smith/differentiable_numerics/field_store.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" #include "smith/differentiable_numerics/state_advancer.hpp" #include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" #include "smith/differentiable_numerics/time_integration_rule.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" #include "smith/differentiable_numerics/differentiable_physics.hpp" #include "smith/physics/weak_form.hpp" #include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/coupling_params.hpp" namespace smith { @@ -34,72 +37,40 @@ namespace smith { * @tparam dim Spatial dimension. * @tparam order Polynomial order for displacement field. * @tparam DisplacementTimeRule Time integration rule type (must have num_states == 4). + * @tparam Coupling Tuple of coupling and parameter packs (default: none). + * Coupling fields occupy leading positions in the tail after the 4 time-rule state fields, + * before user parameter_space fields. * @tparam parameter_space Parameter spaces for material properties. */ template + typename Coupling = std::tuple<>> struct SolidMechanicsSystem : public SystemBase { + using SystemBase::SystemBase; + static_assert(DisplacementTimeRule::num_states == 4, "SolidMechanicsSystem requires a 4-state time integration rule"); - /// using + /// Main weak form: (u, u_old, v_old, a_old, coupling_fields..., params...) using SolidWeakFormType = - TimeDiscretizedWeakForm, - TimeRuleParams, parameter_space...>>; + FunctionalWeakForm, detail::TimeRuleParams, Coupling>>; - /// using -- 3-state form: u, v, a (no u_old needed; at cycle 0 u and v are given, solve for a) - using CycleZeroSolidWeakFormType = - TimeDiscretizedWeakForm, - Parameters, H1, H1, parameter_space...>>; + /// L2 projection weak form for PK1 stress output (dim*dim components). + /// Args: (stress_unknown, u, u_old, v_old, a_old, coupling_fields..., params...). + using StressOutputWeakFormType = FunctionalWeakForm< + dim, L2<0, dim * dim>, + typename detail::AppendCouplingToParams, H1, H1, + H1, H1>>::type>; - std::shared_ptr solid_weak_form; ///< Solid mechanics weak form. - std::shared_ptr - cycle_zero_solid_weak_form; ///< Cycle-zero weak form for initial acceleration solve. + std::shared_ptr solid_weak_form; ///< Solid mechanics weak form. std::shared_ptr disp_bc; ///< Displacement boundary conditions. std::shared_ptr disp_time_rule; ///< Time integration rule. + std::shared_ptr coupling; ///< Coupling metadata for callback interpolation. - /** - * @brief Get the list of all state fields (displacement_solve_state, displacement, velocity, acceleration). - * @return std::vector List of state fields. - */ - std::vector getStateFields() const - { - return {field_store->getField(prefix("displacement_solve_state")), field_store->getField(prefix("displacement")), - field_store->getField(prefix("velocity")), field_store->getField(prefix("acceleration"))}; - } + std::shared_ptr stress_weak_form; ///< Stress projection weak form (nullptr if disabled). + std::shared_ptr stress_output_system; ///< Post-solve system for stress projection. + bool output_cauchy_stress = false; ///< Project Cauchy stress instead of PK1 when true. /** - * @brief Get the list of physical, non-solve state fields. - * @return std::vector List of physical fields suitable for output. - */ - std::vector getOutputFieldStates() const - { - return {field_store->getField(prefix("displacement")), field_store->getField(prefix("velocity")), - field_store->getField(prefix("acceleration"))}; - } - - /** - * @brief Get information about reaction fields for this system. - * @return List of ReactionInfo structures. - */ - std::vector getReactionInfos() const - { - return {{prefix("reactions"), &field_store->getField(prefix("displacement")).get()->space()}}; - } - - /** - * @brief Create a DifferentiablePhysics object for this system. - * @param physics_name The name of the physics. - * @return std::unique_ptr The differentiable physics object. - */ - std::unique_ptr createDifferentiablePhysics(std::string physics_name) - { - return std::make_unique(field_store->getMesh(), field_store->graph(), - field_store->getShapeDisp(), getStateFields(), getParameterFields(), - advancer, physics_name, getReactionInfos()); - } - - /** - * @brief Set the material model for a domain, defining integrals for the solid weak form. + * @brief Set material model for domain. * @tparam MaterialType The material model type. * @param material The material model instance. * @param domain_name The name of the domain to apply the material to. @@ -108,51 +79,49 @@ struct SolidMechanicsSystem : public SystemBase { void setMaterial(const MaterialType& material, const std::string& domain_name) { auto captured_rule = disp_time_rule; - solid_weak_form->addBodyIntegral( - domain_name, [=](auto t_info, auto /*X*/, auto u, auto u_old, auto v_old, auto a_old, auto... params) { - auto [u_current, v_current, a_current] = captured_rule->interpolate(t_info, u, u_old, v_old, a_old); - - typename MaterialType::State state; - auto pk_stress = material(state, get(u_current), params...); - - return smith::tuple{get(a_current) * material.density, pk_stress}; - }); - - // Add to cycle-zero weak form (at cycle 0, u and v are given, solve for a) - cycle_zero_solid_weak_form->addBodyIntegral( - domain_name, [=](auto /*t_info*/, auto /*X*/, auto u, auto /*v_old*/, auto a, auto... params) { - typename MaterialType::State state; - auto pk_stress = material(state, get(u), params...); - - return smith::tuple{get(a) * material.density, pk_stress}; - }); - } - - /** - * @brief Add a body force to the system (with DependsOn). - * @tparam active_parameters Indices of fields this force depends on. - * @tparam BodyForceType The body force function type. - * @param depends_on Dependency specification for which input fields to pass. - * @param domain_name The name of the domain to apply the force to. - * @param force_function The force function (t, X, u, v, a, params...). - */ - template - void addBodyForce(DependsOn depends_on, const std::string& domain_name, - BodyForceType force_function) - { - auto captured_rule = disp_time_rule; - solid_weak_form->addBodySource( - depends_on, domain_name, [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto... params) { - auto [u_current, v_current, a_current] = captured_rule->interpolate(t_info, u, u_old, v_old, a_old); - return force_function(t_info.time(), X, u_current, v_current, a_current, params...); - }); - - addCycleZeroBodySourceImpl( - domain_name, - [=](auto t_info, auto X, auto u, auto v_old, auto a, auto... params) { - return force_function(t_info.time(), X, u, v_old, a, params...); - }, - std::make_index_sequence<3 + sizeof...(parameter_space)>{}); + auto captured_coupling = coupling; + solid_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto u_current, auto v_current, auto a_current, auto... interpolated_params) { + typename MaterialType::State state; + auto pk_stress = + material(t_info, state, get(u_current), get(v_current), interpolated_params...); + return smith::tuple{get(a_current) * material.density, pk_stress}; + }, + raw_args...); + }); + + // Stress output projection: L2 projection of PK1 stress onto an L2 piecewise-constant field. + // Residual: ∫ test · (stress_unknown - pk_stress(u)) dx = 0. + // Args: (stress_unknown, u, u_old, v_old, a_old, params...). stress_unknown is the Jacobian + // variable so the solver builds the mass matrix against it, and the (- pk_stress) term + // becomes the RHS. + if (stress_weak_form) { + bool do_cauchy = this->output_cauchy_stress; + stress_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto stress, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto u_current, auto v_current, auto /*a_current*/, auto... interpolated_params) { + typename MaterialType::State state; + auto pk_stress = material(t_info, state, get(u_current), get(v_current), + interpolated_params...); + + auto flat_stress = [&]() { + if (do_cauchy) { + static constexpr auto I_ = Identity(); + auto F = get(u_current) + I_; + auto J = det(F); + auto sigma = (1.0 / J) * dot(pk_stress, transpose(F)); + return make_tensor([&](int i) { return sigma[i / dim][i % dim]; }); + } + return make_tensor([&](int i) { return pk_stress[i / dim][i % dim]; }); + }(); + return smith::tuple{get(stress) - flat_stress, tensor{}}; + }, + raw_args...); + }); + } } /** @@ -163,36 +132,17 @@ struct SolidMechanicsSystem : public SystemBase { */ template void addBodyForce(const std::string& domain_name, BodyForceType force_function) - { - addBodyForceAllParams(domain_name, force_function, std::make_index_sequence<4 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a surface traction (flux) to the system (with DependsOn). - * @tparam active_parameters Indices of fields this traction depends on. - * @tparam TractionType The traction function type. - * @param depends_on Dependency specification for which input fields to pass. - * @param domain_name The name of the boundary domain to apply the traction to. - * @param traction_function The traction function (t, X, n, u, v, a, params...). - */ - template - void addTraction(DependsOn depends_on, const std::string& domain_name, - TractionType traction_function) { auto captured_rule = disp_time_rule; - solid_weak_form->addBoundaryFlux( - depends_on, domain_name, - [=](auto t_info, auto X, auto n, auto u, auto u_old, auto v_old, auto a_old, auto... params) { - auto [u_current, v_current, a_current] = captured_rule->interpolate(t_info, u, u_old, v_old, a_old); - return traction_function(t_info.time(), X, n, u_current, v_current, a_current, params...); - }); - - addCycleZeroBoundaryFluxImpl( - domain_name, - [=](auto t_info, auto X, auto n, auto u, auto v_old, auto a, auto... params) { - return traction_function(t_info.time(), X, n, u, v_old, a, params...); - }, - std::make_index_sequence<3 + sizeof...(parameter_space)>{}); + auto captured_coupling = coupling; + solid_weak_form->addBodySource(domain_name, [=](auto t_info, auto X, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto u_current, auto v_current, auto a_current, auto... interpolated_params) { + return force_function(t_info.time(), X, u_current, v_current, a_current, interpolated_params...); + }, + raw_args...); + }); } /** @@ -203,50 +153,17 @@ struct SolidMechanicsSystem : public SystemBase { */ template void addTraction(const std::string& domain_name, TractionType traction_function) - { - addTractionAllParams(domain_name, traction_function, std::make_index_sequence<4 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a pressure boundary condition (follower force) (with DependsOn). - * @tparam active_parameters Indices of fields this pressure depends on. - * @tparam PressureType The pressure function type. - * @param depends_on Dependency specification for which input fields to pass. - * @param domain_name The name of the boundary domain. - * @param pressure_function The pressure function (t, X, params...). - */ - template - void addPressure(DependsOn depends_on, const std::string& domain_name, - PressureType pressure_function) { auto captured_rule = disp_time_rule; - solid_weak_form->addBoundaryIntegral( - depends_on, domain_name, [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto... params) { - auto u_current = captured_rule->value(t_info, u, u_old, v_old, a_old); - - auto x_current = X + u_current; - auto n_deformed = cross(get(x_current)); - auto n_shape_norm = norm(cross(get(X))); - - auto pressure = pressure_function(t_info.time(), get(X), get(params)...); - - return pressure * n_deformed * (1.0 / n_shape_norm); - }); - - addCycleZeroBoundaryIntegralImpl( - domain_name, - [=](auto t_info, auto X, auto u, auto /*v_old*/, auto /*a*/, auto... params) { - auto u_current = u; - - auto x_current = X + u_current; - auto n_deformed = cross(get(x_current)); - auto n_shape_norm = norm(cross(get(X))); - - auto pressure = pressure_function(t_info.time(), get(X), get(params)...); - - return pressure * n_deformed * (1.0 / n_shape_norm); - }, - std::make_index_sequence<3 + sizeof...(parameter_space)>{}); + auto captured_coupling = coupling; + solid_weak_form->addBoundaryFlux(domain_name, [=](auto t_info, auto X, auto n, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto u_current, auto v_current, auto a_current, auto... interpolated_params) { + return traction_function(t_info.time(), X, n, u_current, v_current, a_current, interpolated_params...); + }, + raw_args...); + }); } /** @@ -258,163 +175,319 @@ struct SolidMechanicsSystem : public SystemBase { template void addPressure(const std::string& domain_name, PressureType pressure_function) { - addPressureAllParams(domain_name, pressure_function, std::make_index_sequence<4 + sizeof...(parameter_space)>{}); + auto captured_rule = disp_time_rule; + auto captured_coupling = coupling; + solid_weak_form->addBoundaryIntegral(domain_name, [=](auto t_info, auto X, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto u_current, auto /*v_current*/, auto /*a_current*/, auto... interpolated_params) { + auto x_current = X + u_current; + auto n_deformed = cross(get(x_current)); + auto n_shape_norm = norm(cross(get(X))); + + auto pressure = pressure_function(t_info.time(), get(X), get(interpolated_params)...); + + return pressure * n_deformed * (1.0 / n_shape_norm); + }, + raw_args...); + }); } - private: - template - void addBodyForceAllParams(const std::string& domain_name, BodyForceType force_function, std::index_sequence) - { - addBodyForce(DependsOn(Is)...>{}, domain_name, force_function); - } + /// Set zero-displacement Dirichlet BC on all components. + void setDisplacementBC(const Domain& domain) { disp_bc->template setFixedVectorBCs(domain); } - template - void addTractionAllParams(const std::string& domain_name, TractionType traction_function, std::index_sequence) + /// Set zero-displacement BC on specific components. + void setDisplacementBC(const Domain& domain, std::vector components) { - addTraction(DependsOn(Is)...>{}, domain_name, traction_function); + disp_bc->template setFixedVectorBCs(domain, components); } - template - void addPressureAllParams(const std::string& domain_name, PressureType pressure_function, std::index_sequence) + /// Set displacement BC with a prescribed function. + template + void setDisplacementBC(const Domain& domain, AppliedDisplacementFunction f) { - addPressure(DependsOn(Is)...>{}, domain_name, pressure_function); + disp_bc->template setVectorBCs(domain, f); } - // Cycle-zero helpers: always use all-params DependsOn with the 3-state cycle-zero form count - template - void addCycleZeroBodySourceImpl(const std::string& name, IntegrandType f, std::index_sequence) - { - cycle_zero_solid_weak_form->addBodySource(DependsOn(Is)...>{}, name, f); - } + private: +}; - template - void addCycleZeroBoundaryFluxImpl(const std::string& name, IntegrandType f, std::index_sequence) - { - cycle_zero_solid_weak_form->addBoundaryFlux(DependsOn(Is)...>{}, name, f); +/** + * @brief Optional auxiliary systems and outputs for solid mechanics. + */ +struct SolidMechanicsOptions { + bool enable_stress_output = false; ///< Register stress output fields during phase 1. + bool output_cauchy_stress = false; ///< When true, project Cauchy stress (sigma) instead of PK1 (P). +}; + +/** + * @brief Register all solid mechanics fields into a FieldStore. + * + * Phase 1 of the two-phase initialization. + * + * @return PhysicsFields carrying the exported field tokens and time rule type. + */ +template +auto registerSolidMechanicsFields(std::shared_ptr field_store, + const SolidMechanicsOptions& options = SolidMechanicsOptions{}) +{ + FieldType> shape_disp_type("shape_displacement"); + if (!field_store->hasField(field_store->prefix(shape_disp_type.name))) { + field_store->addShapeDisp(shape_disp_type); } - template - void addCycleZeroBoundaryIntegralImpl(const std::string& name, IntegrandType f, std::index_sequence) - { - cycle_zero_solid_weak_form->addBoundaryIntegral(DependsOn(Is)...>{}, name, f); + auto disp_time_rule_ptr = std::make_shared(); + FieldType> disp_type("displacement_solve_state"); + field_store->addIndependent(disp_type, disp_time_rule_ptr); + + field_store->addDependent(disp_type, FieldStore::TimeDerivative::VAL, "displacement"); + field_store->addDependent(disp_type, FieldStore::TimeDerivative::DOT, "velocity"); + field_store->addDependent(disp_type, FieldStore::TimeDerivative::DDOT, "acceleration"); + + auto physics_fields = + PhysicsFields, H1, H1, H1>{ + field_store, FieldType>(field_store->prefix("displacement_solve_state")), + FieldType>(field_store->prefix("displacement")), + FieldType>(field_store->prefix("velocity")), + FieldType>(field_store->prefix("acceleration"))}; + + if (options.enable_stress_output) { + auto stress_time_rule = std::make_shared(); + FieldType> stress_type("stress"); + field_store->addIndependent(stress_type, stress_time_rule); } -}; + return physics_fields; +} /** - * @brief Factory function to build a solid dynamics system with configurable time integration. - * @tparam dim Spatial dimension. - * @tparam order Polynomial order for displacement field. - * @tparam DisplacementTimeRule Time integration rule type (must have num_states == 4, deduced from argument). - * @tparam parameter_space Parameter spaces for material properties. - * @param mesh The mesh. - * @param solver The coupled system solver. - * @param disp_time_rule The time integration rule. - * @param prepend_name The name of the physics (used as field prefix). - * @param cycle_zero_solver Optional override for the cycle-zero solve. Defaults to - * `solver->singleBlockSolver(0)`. - * @param parameter_types Parameter field types. - * @return SolidMechanicsSystem with all components initialized. + * @brief Internal solid mechanics builder after coupling fields are assembled. + * + * Phase 2 of the two-phase initialization. */ -template -SolidMechanicsSystem buildSolidMechanicsSystem( - std::shared_ptr mesh, std::shared_ptr solver, DisplacementTimeRule disp_time_rule, - std::string prepend_name = "", std::shared_ptr cycle_zero_solver = nullptr, - FieldType... parameter_types) +namespace detail { + +/// @brief Return true when stress output fields were registered during phase 1. +inline bool hasRegisteredStressOutput(const std::shared_ptr& field_store) { - auto field_store = std::make_shared(mesh, 100); + return field_store->hasField(field_store->prefix("stress")); +} - auto prefix = [&](const std::string& name) { - if (prepend_name.empty()) { - return name; +/// @brief Build a cycle-zero solver from the main solver when possible, else use fallback defaults. +inline std::shared_ptr makeCycleZeroSolver(std::shared_ptr solver, const Mesh& mesh) +{ + if (solver) { + if (auto derived_solver = solver->singleBlockSolver(0)) { + return derived_solver; } - return prepend_name + "_" + name; - }; - - // Add shape displacement - FieldType> shape_disp_type(prefix("shape_displacement")); - field_store->addShapeDisp(shape_disp_type); - - // Add displacement as independent (unknown) with time integration rule - auto disp_time_rule_ptr = std::make_shared(disp_time_rule); - FieldType> disp_type(prefix("displacement_solve_state")); - auto disp_bc = field_store->addIndependent(disp_type, disp_time_rule_ptr); - - // Add dependent fields for time integration history - auto disp_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::VAL, prefix("displacement")); - auto velo_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DOT, prefix("velocity")); - auto accel_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DDOT, prefix("acceleration")); - - // Add parameters - std::vector parameter_fields; - (field_store->addParameter(FieldType(prefix("param_" + parameter_types.name))), ...); - (parameter_fields.push_back(field_store->getField(prefix("param_" + parameter_types.name))), ...); - - using SystemType = SolidMechanicsSystem; - - // Create solid mechanics weak form (u, u_old, v_old, a_old) - std::string force_name = prefix("solid_force"); - auto solid_weak_form = std::make_shared( - force_name, field_store->getMesh(), field_store->getField(disp_type.name).get()->space(), - field_store->createSpaces(force_name, disp_type.name, disp_type, disp_old_type, velo_old_type, accel_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); - - // Create cycle-zero weak form (u, v, a) for initial acceleration solve — 3 states, no u_old - std::string cycle_zero_name = prefix("solid_reaction"); - auto cycle_zero_solid_weak_form = std::make_shared( - cycle_zero_name, field_store->getMesh(), field_store->getField(accel_old_type.name).get()->space(), - field_store->createSpaces(cycle_zero_name, accel_old_type.name, disp_type, velo_old_type, accel_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); - - if (cycle_zero_solver == nullptr) { - cycle_zero_solver = solver->singleBlockSolver(0); } - SLIC_ERROR_IF(cycle_zero_solver == nullptr, - "Could not derive a cycle-zero solver for block 0 from the provided solid mechanics solver."); - - // Build advancer - std::vector> weak_forms{solid_weak_form}; - auto advancer = std::make_shared(field_store, weak_forms, solver, - cycle_zero_solid_weak_form, cycle_zero_solver); - - return SystemType{{field_store, solver, advancer, parameter_fields, prepend_name}, - solid_weak_form, - cycle_zero_solid_weak_form, - disp_bc, - disp_time_rule_ptr}; + + NonlinearSolverOptions cycle_zero_nonlin{.nonlin_solver = NonlinearSolver::Newton, + .relative_tol = 1e-14, + .absolute_tol = 1e-14, + .max_iterations = 2, + .print_level = 0}; + LinearSolverOptions cycle_zero_lin{.linear_solver = LinearSolver::CG, + .preconditioner = Preconditioner::HypreJacobi, + .relative_tol = 1e-14, + .absolute_tol = 1e-14, + .max_iterations = 1000, + .print_level = 0}; + return std::make_shared(buildNonlinearBlockSolver(cycle_zero_nonlin, cycle_zero_lin, mesh)); +} + +/** + * @brief Internal solid builder after public registration and coupling collection. + */ +template + requires detail::is_coupling_packs_v +auto buildSolidMechanicsSystemImpl(std::shared_ptr field_store, const Coupling& coupling, + std::shared_ptr solver, const SolidMechanicsOptions& options, + bool has_stress_output) +{ + auto disp_time_rule_ptr = std::make_shared(); + + FieldType> shape_disp_type(field_store->prefix("shape_displacement")); + FieldType> disp_type(field_store->prefix("displacement_solve_state"), true); + FieldType> disp_old_type(field_store->prefix("displacement")); + FieldType> velo_old_type(field_store->prefix("velocity")); + FieldType> accel_old_type(field_store->prefix("acceleration")); + + auto disp_bc = field_store->getBoundaryConditions(disp_type.name); + + using SystemType = SolidMechanicsSystem; + + auto coupling_fields_flat = detail::flattenCouplingFields(coupling); + std::string force_name = field_store->prefix("reactions"); + auto solid_weak_form = detail::buildWeakFormWithCoupling( + field_store, force_name, disp_type.name, disp_type, disp_old_type, velo_old_type, accel_old_type, + coupling_fields_flat); + + auto sys = std::make_shared(field_store, solver, std::vector>{solid_weak_form}); + sys->disp_bc = disp_bc; + sys->disp_time_rule = disp_time_rule_ptr; + sys->coupling = std::make_shared(coupling); + sys->solid_weak_form = solid_weak_form; + sys->output_cauchy_stress = options.output_cauchy_stress; + + if (disp_time_rule_ptr->requiresInitialAccelerationSolve()) { + std::string cycle_zero_name = field_store->prefix("cycle_zero_acceleration_reaction"); + field_store->markWeakFormInternal(cycle_zero_name); + auto cycle_zero_solver = detail::makeCycleZeroSolver(solver, *field_store->getMesh()); + + auto cycle_zero_system = makeSystem(field_store, cycle_zero_solver, {sys->solid_weak_form}); + cycle_zero_system->solve_result_field_names = {accel_old_type.name}; + auto cycle_zero_inputs = + std::vector{disp_old_type.name, disp_old_type.name, velo_old_type.name, accel_old_type.name}; + auto append_if_state = [&](const auto& field) { + const auto& states = field_store->getStateFields(); + if (std::any_of(states.begin(), states.end(), + [&](const auto& state) { return state.get()->name() == field.name; })) { + cycle_zero_inputs.push_back(field.name); + } + }; + std::apply([&](const auto&... coupling_fields) { (append_if_state(coupling_fields), ...); }, coupling_fields_flat); + cycle_zero_system->solve_input_field_names = {cycle_zero_inputs}; + sys->cycle_zero_systems.push_back(cycle_zero_system); + } + + if (has_stress_output) { + FieldType> stress_type(field_store->prefix("stress"), true); + FieldType> disp_as_input(disp_type.name); + std::string stress_name = field_store->prefix("stress_projection"); + sys->stress_weak_form = detail::buildWeakFormWithCoupling( + field_store, stress_name, stress_type.name, stress_type, disp_as_input, disp_old_type, velo_old_type, + accel_old_type, coupling_fields_flat); + + NonlinearSolverOptions stress_nonlin{.nonlin_solver = NonlinearSolver::Newton, + .relative_tol = 1e-14, + .absolute_tol = 1e-14, + .max_iterations = 2, + .print_level = 0}; + LinearSolverOptions stress_lin{.linear_solver = LinearSolver::CG, + .preconditioner = Preconditioner::HypreJacobi, + .relative_tol = 1e-14, + .absolute_tol = 1e-14, + .max_iterations = 1000, + .print_level = 0}; + auto stress_solver = + std::make_shared(buildNonlinearBlockSolver(stress_nonlin, stress_lin, *field_store->getMesh())); + + sys->stress_output_system = makeSystem(field_store, stress_solver, {sys->stress_weak_form}); + sys->post_solve_systems.push_back(sys->stress_output_system); + } + + return sys; +} + +} // namespace detail + +/** + * @brief Build a SolidMechanicsSystem from already-registered field packs. + * + * Deduces the displacement time rule from `self_fields`. + * + * Usage: + * @code + * auto solid_system = buildSolidMechanicsSystem( + * solver, opts, solid_fields, couplingFields(thermal_fields), param_fields); + * @endcode + */ +template + requires(detail::has_time_rule_v) +auto buildSolidMechanicsSystem(std::shared_ptr solver, const SolidMechanicsOptions& options, + const SelfFields& self_fields) +{ + constexpr int dim = SelfFields::dim; + constexpr int order = SelfFields::order; + using DisplacementTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(); + bool has_stress_output = detail::hasRegisteredStressOutput(field_store); + return detail::buildSolidMechanicsSystemImpl(field_store, coupling, solver, options, + has_stress_output); +} + +/** + * @brief Build a SolidMechanicsSystem from registered self fields plus coupled physics fields. + */ +template + requires(detail::has_time_rule_v) +auto buildSolidMechanicsSystem(std::shared_ptr solver, const SolidMechanicsOptions& options, + const SelfFields& self_fields, const CouplingFields& coupled) +{ + constexpr int dim = SelfFields::dim; + constexpr int order = SelfFields::order; + using DisplacementTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled); + bool has_stress_output = detail::hasRegisteredStressOutput(field_store); + return detail::buildSolidMechanicsSystemImpl(field_store, coupling, solver, options, + has_stress_output); } /** - * @brief Factory function to build a solid dynamics system with a physics name and parameter fields. + * @brief Build a SolidMechanicsSystem from registered self fields plus registered parameter fields. */ -template -SolidMechanicsSystem buildSolidMechanicsSystem( - std::shared_ptr mesh, std::shared_ptr solver, DisplacementTimeRule disp_time_rule, - std::string prepend_name, FieldType... parameter_types) +template + requires(detail::has_time_rule_v) +auto buildSolidMechanicsSystem(std::shared_ptr solver, const SolidMechanicsOptions& options, + const SelfFields& self_fields, const ParamFields& params) { - return buildSolidMechanicsSystem(mesh, solver, disp_time_rule, std::move(prepend_name), nullptr, - parameter_types...); + constexpr int dim = SelfFields::dim; + constexpr int order = SelfFields::order; + using DisplacementTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(params); + bool has_stress_output = detail::hasRegisteredStressOutput(field_store); + return detail::buildSolidMechanicsSystemImpl(field_store, coupling, solver, options, + has_stress_output); } /** - * @brief Factory function to build a solid dynamics system (without physics name). + * @brief Build a SolidMechanicsSystem from registered self fields, coupled physics fields, and parameter fields. */ -template -SolidMechanicsSystem buildSolidMechanicsSystem( - std::shared_ptr mesh, std::shared_ptr solver, DisplacementTimeRule disp_time_rule, - std::shared_ptr cycle_zero_solver, FieldType... parameter_types) +template + requires(detail::has_time_rule_v) +auto buildSolidMechanicsSystem(std::shared_ptr solver, const SolidMechanicsOptions& options, + const SelfFields& self_fields, const CouplingFields& coupled, + const ParamFields& params) { - return buildSolidMechanicsSystem(mesh, solver, disp_time_rule, "", cycle_zero_solver, parameter_types...); + constexpr int dim = SelfFields::dim; + constexpr int order = SelfFields::order; + using DisplacementTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled, params); + bool has_stress_output = detail::hasRegisteredStressOutput(field_store); + return detail::buildSolidMechanicsSystemImpl(field_store, coupling, solver, options, + has_stress_output); } /** - * @brief Factory function to build a solid dynamics system (without physics name). + * @brief Build a SolidMechanicsSystem from solver options and a mesh, registering parameter fields inline. + * + * Constructs the FieldStore, nonlinear block solver, system solver, and registers solid mechanics + * and parameter fields, then delegates to the existing field-pack overload. + * + * @tparam dim Spatial dimension. + * @tparam order Polynomial order for displacement. + * @tparam DisplacementTimeRule Time integration rule (defaults to ImplicitNewmarkSecondOrderTimeIntegrationRule). + * @tparam ParamSpaces Parameter function-space tags deduced from FieldType arguments. */ -template -SolidMechanicsSystem buildSolidMechanicsSystem( - std::shared_ptr mesh, std::shared_ptr solver, DisplacementTimeRule disp_time_rule, - FieldType... parameter_types) +template +auto buildSolidMechanicsSystem(const NonlinearSolverOptions& nonlinear_opts, const LinearSolverOptions& linear_opts, + const SolidMechanicsOptions& options, std::shared_ptr mesh, + FieldType... params) { - return buildSolidMechanicsSystem(mesh, solver, disp_time_rule, "", nullptr, parameter_types...); + auto field_store = std::make_shared(mesh); + auto solver = std::make_shared(buildNonlinearBlockSolver(nonlinear_opts, linear_opts, *mesh)); + auto solid_fields = registerSolidMechanicsFields(field_store, options); + if constexpr (sizeof...(ParamSpaces) > 0) { + auto param_fields = registerParameterFields(field_store, std::move(params)...); + return buildSolidMechanicsSystem(solver, options, solid_fields, param_fields); + } else { + return buildSolidMechanicsSystem(solver, options, solid_fields); + } } } // namespace smith diff --git a/src/smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp b/src/smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp index 2e52f7fea0..68d2a82d81 100644 --- a/src/smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp +++ b/src/smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp @@ -6,481 +6,124 @@ /** * @file solid_mechanics_with_internal_vars_system.hpp - * @brief Defines the SolidMechanicsWithInternalVarsSystem struct and its factory function + * @brief Helpers to wire internal-variable materials to composed solid and internal-variable systems. */ #pragma once -#include "smith/differentiable_numerics/field_store.hpp" -#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" -#include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" -#include "smith/differentiable_numerics/state_advancer.hpp" -#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" -#include "smith/differentiable_numerics/time_integration_rule.hpp" -#include "smith/numerics/functional/tuple.hpp" -#include "smith/numerics/functional/tensor.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" -#include "smith/differentiable_numerics/differentiable_physics.hpp" -#include "smith/physics/weak_form.hpp" -#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/state_variable_system.hpp" namespace smith { -/** - * @brief System struct for solid mechanics with an additional internal variable (L2 state). - * - * Displacement uses a 4-state second-order layout (displacement_solve_state, displacement, velocity, acceleration). - * Internal variable uses a 2-state first-order layout (state_solve_state, state). - * Total: 6 state fields. - * - * @tparam dim Spatial dimension. - * @tparam disp_order Polynomial order for displacement field. - * @tparam StateSpace Finite element space for the internal variable (e.g., L2). - * @tparam DisplacementTimeRule Time integration rule for displacement (must have num_states == 4). - * @tparam InternalVarTimeRule Time integration rule for the internal variable (must have num_states == 2). - * @tparam parameter_space Parameter spaces for material properties. - */ -template -struct SolidMechanicsWithInternalVarsSystem : public SystemBase { - static_assert(DisplacementTimeRule::num_states == 4, - "SolidMechanicsWithInternalVarsSystem requires a 4-state displacement rule"); - static_assert(InternalVarTimeRule::num_states == 2, - "SolidMechanicsWithInternalVarsSystem requires a 2-state internal variable rule"); - - // Primary weak form: residual for displacement (u). - // Inputs: u, u_old, v_old, a_old, alpha, alpha_old, params... - /// using - using SolidWeakFormType = - TimeDiscretizedWeakForm, - Parameters, H1, H1, - H1, StateSpace, StateSpace, parameter_space...>>; - - // State weak form: residual for internal variable (alpha). - // Inputs: alpha, alpha_old, u, u_old, v_old, a_old, params... - /// using - using StateWeakFormType = - TimeDiscretizedWeakForm, H1, - H1, H1, parameter_space...>>; - - // Cycle-zero weak form: test field = acceleration, inputs: u, v, a, alpha, params... - /// using - using CycleZeroSolidWeakFormType = TimeDiscretizedWeakForm< - dim, H1, - Parameters, H1, H1, StateSpace, parameter_space...>>; - - std::shared_ptr solid_weak_form; ///< Solid mechanics weak form. - std::shared_ptr state_weak_form; ///< Internal variable weak form. - std::shared_ptr cycle_zero_solid_weak_form; ///< Cycle-zero weak form. - std::shared_ptr disp_bc; ///< Displacement boundary conditions. - std::shared_ptr state_bc; ///< Internal variable boundary conditions. - std::shared_ptr disp_time_rule; ///< Time integration for displacement. - std::shared_ptr state_time_rule; ///< Time integration for internal variable. - - /** - * @brief Get the list of all state fields (disp_pred, disp, vel, accel, state_pred, state). - * @return std::vector List of state fields. - */ - std::vector getStateFields() const - { - return {field_store->getField(prefix("displacement_solve_state")), - field_store->getField(prefix("displacement")), - field_store->getField(prefix("velocity")), - field_store->getField(prefix("acceleration")), - field_store->getField(prefix("state_solve_state")), - field_store->getField(prefix("state"))}; - } - - /** - * @brief Get the list of physical, non-solve state fields. - * @return std::vector List of physical fields suitable for output. - */ - std::vector getOutputFieldStates() const - { - return {field_store->getField(prefix("displacement")), field_store->getField(prefix("velocity")), - field_store->getField(prefix("acceleration")), field_store->getField(prefix("state"))}; - } - - /** - * @brief Get information about reaction fields for this system. - * @return List of ReactionInfo structures. - */ - std::vector getReactionInfos() const - { - return {{prefix("solid_residual"), &field_store->getField(prefix("displacement")).get()->space()}, - {prefix("state_residual"), &field_store->getField(prefix("state")).get()->space()}}; - } - - /** - * @brief Create a DifferentiablePhysics object for this system. - * @param physics_name The name of the physics. - * @return std::unique_ptr The differentiable physics object. - */ - std::unique_ptr createDifferentiablePhysics(std::string physics_name) - { - return std::make_unique(field_store->getMesh(), field_store->graph(), - field_store->getShapeDisp(), getStateFields(), getParameterFields(), - advancer, physics_name, getReactionInfos()); - } - - /** - * @brief Set the material model for the solid mechanics part. - * - * The material is called as material(state, grad_u_current, alpha_current, params...) and - * must expose a `density` member for the cycle-zero acceleration solve. - * - * @tparam MaterialType The material model type. - * @param material The material model instance. - * @param domain_name The name of the domain to apply the material to. - */ - template - void setMaterial(const MaterialType& material, const std::string& domain_name) - { - auto captured_disp_rule = disp_time_rule; - auto captured_state_rule = state_time_rule; - - solid_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto u, auto u_old, auto v_old, - auto a_old, auto alpha, auto alpha_old, auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto alpha_current = captured_state_rule->value(t_info, alpha, alpha_old); - - typename MaterialType::State state; - auto pk_stress = material(state, get(u_current), get(alpha_current), params...); - - return smith::tuple{get(a_current) * material.density, pk_stress}; - }); - - // Cycle-zero: u and v are given, solve for a; alpha at initial condition - cycle_zero_solid_weak_form->addBodyIntegral( - domain_name, [=](auto /*t_info*/, auto /*X*/, auto u, auto /*v*/, auto a, auto alpha, auto... params) { - auto alpha_current = alpha; // at cycle 0, use initial alpha - typename MaterialType::State state; - auto pk_stress = material(state, get(u), get(alpha_current), params...); - - return smith::tuple{get(a) * material.density, pk_stress}; - }); - } - - /** - * @brief Add a body force to the solid mechanics part (with DependsOn). - * @param depends_on Selects which primal and parameter fields the contribution depends on. - * @param domain_name The name of the domain where the body force is applied. - * @param force_function (t, X, u, v, a, alpha, alpha_dot, params...) -> force vector. - */ - template - void addBodyForce(DependsOn depends_on, const std::string& domain_name, - BodyForceType force_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_state_rule = state_time_rule; - solid_weak_form->addBodySource( - depends_on, domain_name, - [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto alpha, auto alpha_old, - auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [alpha_current, alpha_dot] = captured_state_rule->interpolate(t_info, alpha, alpha_old); - return force_function(t_info.time(), X, u_current, v_current, a_current, alpha_current, alpha_dot, params...); - }); - - addCycleZeroBodySourceImpl( - domain_name, - [=](auto t_info, auto X, auto u, auto v, auto a, auto alpha, auto... params) { - auto alpha_dot = 0.0 * alpha; - return force_function(t_info.time(), X, u, v, a, alpha, alpha_dot, params...); - }, - std::make_index_sequence<4 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a body force that depends on all state and parameter fields. - * @param domain_name The name of the domain where the body force is applied. - * @param force_function (t, X, u, v, a, alpha, alpha_dot, params...) -> force vector. - */ - template - void addBodyForce(const std::string& domain_name, BodyForceType force_function) - { - addBodyForceAllParams(domain_name, force_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a surface traction to the solid mechanics part (with DependsOn). - * @param depends_on Selects which primal and parameter fields the contribution depends on. - * @param domain_name The name of the boundary where the traction is applied. - * @param traction_function (t, X, n, u, v, a, alpha, alpha_dot, params...) -> traction vector. - */ - template - void addTraction(DependsOn depends_on, const std::string& domain_name, - TractionType traction_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_state_rule = state_time_rule; - solid_weak_form->addBoundaryFlux( - depends_on, domain_name, - [=](auto t_info, auto X, auto n, auto u, auto u_old, auto v_old, auto a_old, auto alpha, auto alpha_old, - auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [alpha_current, alpha_dot] = captured_state_rule->interpolate(t_info, alpha, alpha_old); - return traction_function(t_info.time(), X, n, u_current, v_current, a_current, alpha_current, alpha_dot, - params...); - }); - - addCycleZeroBoundaryFluxImpl( - domain_name, - [=](auto t_info, auto X, auto n, auto u, auto v, auto a, auto alpha, auto... params) { - auto alpha_dot = 0.0 * alpha; - return traction_function(t_info.time(), X, n, u, v, a, alpha, alpha_dot, params...); - }, - std::make_index_sequence<4 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a surface traction that depends on all state and parameter fields. - * @param domain_name The name of the boundary where the traction is applied. - * @param traction_function (t, X, n, u, v, a, alpha, alpha_dot, params...) -> traction vector. - */ - template - void addTraction(const std::string& domain_name, TractionType traction_function) - { - addTractionAllParams(domain_name, traction_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a pressure boundary condition (follower force) (with DependsOn). - * @param depends_on Selects which primal and parameter fields the contribution depends on. - * @param domain_name The name of the boundary where the pressure is applied. - * @param pressure_function (t, X, u, v, a, alpha, alpha_dot, params...) -> pressure scalar. - */ - template - void addPressure(DependsOn depends_on, const std::string& domain_name, - PressureType pressure_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_state_rule = state_time_rule; - solid_weak_form->addBoundaryIntegral( - depends_on, domain_name, - [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto alpha, auto alpha_old, - auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [alpha_current, alpha_dot] = captured_state_rule->interpolate(t_info, alpha, alpha_old); - - auto x_current = X + u_current; - auto n_deformed = cross(get(x_current)); - auto n_shape_norm = norm(cross(get(X))); - - auto pressure = pressure_function(t_info.time(), get(X), u_current, v_current, a_current, - alpha_current, alpha_dot, get(params)...); - - return pressure * n_deformed * (1.0 / n_shape_norm); - }); +namespace detail { - addCycleZeroBoundaryIntegralImpl( - domain_name, - [=](auto t_info, auto X, auto u, auto v, auto a, auto alpha, auto... params) { - auto alpha_val = get(alpha); - auto alpha_dot = 0.0 * alpha_val; - - auto x_current = X + u; - auto n_deformed = cross(get(x_current)); - auto n_shape_norm = norm(cross(get(X))); - - auto pressure = pressure_function(t_info.time(), get(X), get(u), get(v), get(a), - alpha_val, alpha_dot, get(params)...); - - return pressure * n_deformed * (1.0 / n_shape_norm); - }, - std::make_index_sequence<4 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a pressure boundary condition that depends on all state and parameter fields. - * @param domain_name The name of the boundary where the pressure is applied. - * @param pressure_function (t, X, u, v, a, alpha, alpha_dot, params...) -> pressure scalar. - */ - template - void addPressure(const std::string& domain_name, PressureType pressure_function) - { - addPressureAllParams(domain_name, pressure_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add the evolution law for the internal variable. - * @tparam EvolutionType The evolution law function type. - * @param domain_name The name of the domain. - * @param evolution_law Function (t_info, alpha, alpha_dot, grad_u, params...) returning the ODE residual. - */ - template - void addStateEvolution(const std::string& domain_name, EvolutionType evolution_law) - { - auto captured_disp_rule = disp_time_rule; - auto captured_state_rule = state_time_rule; - - state_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto alpha, auto alpha_old, auto u, - auto u_old, auto v_old, auto a_old, auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [alpha_current, alpha_dot] = captured_state_rule->interpolate(t_info, alpha, alpha_old); - - auto residual_val = evolution_law(t_info, get(alpha_current), get(alpha_dot), - get(u_current), params...); - - tensor flux{}; - return smith::tuple{residual_val, flux}; - }); +template +/// @brief Dispatch internal-variable material calls with or without explicit `TimeInfo`. +auto evaluateCoupledInternalVariableMaterial(const MaterialType& material, const TimeInfo& t_info, AlphaType alpha, + AlphaDotType alpha_dot, DerivType deriv_u, ParamTypes&&... params) +{ + if constexpr (requires { material(t_info, alpha, alpha_dot, deriv_u, std::forward(params)...); }) { + return material(t_info, alpha, alpha_dot, deriv_u, std::forward(params)...); + } else { + return material(alpha, alpha_dot, deriv_u, std::forward(params)...); } +} - private: - template - void addBodyForceAllParams(const std::string& domain_name, BodyForceType force_function, std::index_sequence) - { - addBodyForce(DependsOn(Is)...>{}, domain_name, force_function); - } +template +/// @brief Evaluate solid/internal-variable material using TimeInfo-aware signature. +auto evaluateSolidInternalVariableMaterial(const MaterialType& material, const TimeInfo& t_info, StateType& state, + const GradUType& grad_u, const GradVType& grad_v, AlphaType alpha, + ParamTypes&&... params) +{ + return material(t_info, state, grad_u, grad_v, alpha, std::forward(params)...); +} - template - void addTractionAllParams(const std::string& domain_name, TractionType traction_function, std::index_sequence) - { - addTraction(DependsOn(Is)...>{}, domain_name, traction_function); - } +template +/// @brief Adapts coupled internal-variable solids to solid-system material interface. +struct CoupledSolidInternalVariableMaterialAdapter { + /// Material state type forwarded to solid system. + using State = typename MaterialType::State; - template - void addPressureAllParams(const std::string& domain_name, PressureType pressure_function, std::index_sequence) - { - addPressure(DependsOn(Is)...>{}, domain_name, pressure_function); - } + MaterialType material; ///< Wrapped constitutive model. + double density; ///< Material density exposed for solid residual. - // Cycle-zero helpers: use all-params DependsOn with the 5-state cycle-zero form (u, v, a, alpha, alpha_old) - template - void addCycleZeroBodySourceImpl(const std::string& name, IntegrandType f, std::index_sequence) + template + /// @brief Evaluate wrapped material with current internal-variable value. + auto operator()(const TimeInfo& t_info, StateType& state, GradUType grad_u, GradVType grad_v, AlphaType alpha, + AlphaDotType /*alpha_dot*/, ParamTypes&&... params) const { - cycle_zero_solid_weak_form->addBodySource(DependsOn(Is)...>{}, name, f); - } - - template - void addCycleZeroBoundaryFluxImpl(const std::string& name, IntegrandType f, std::index_sequence) - { - cycle_zero_solid_weak_form->addBoundaryFlux(DependsOn(Is)...>{}, name, f); - } - - template - void addCycleZeroBoundaryIntegralImpl(const std::string& name, IntegrandType f, std::index_sequence) - { - cycle_zero_solid_weak_form->addBoundaryIntegral(DependsOn(Is)...>{}, name, f); + return evaluateSolidInternalVariableMaterial(material, t_info, state, grad_u, grad_v, get(alpha), + std::forward(params)...); } }; +} // namespace detail + /** - * @brief Factory function to build a solid mechanics system with internal variable. - * @param mesh The mesh. - * @param solver The coupled system solver. - * @param disp_rule The displacement time integration rule. - * @param state_rule The internal-variable time integration rule. - * @param prepend_name Optional field-name prefix. - * @param cycle_zero_solver Optional override for the cycle-zero solve. Defaults to - * `solver->singleBlockSolver(0)`. - * @param parameter_types Optional parameter field descriptors. + * @brief Register a solid material integrand on a SolidMechanicsSystem that is coupled to + * an InternalVariableSystem carrying internal variables. + * + * Assumes: + * - solid was built with state fields as leading coupling fields + * (first 2 coupling positions: state_solve_state, state). + * - state was built with solid displacement fields as leading coupling fields + * (first 4 coupling positions: displacement_solve_state, displacement, velocity, acceleration). + * + * Solid material callable must satisfy: + * material(t_info, state, grad_u, grad_v, alpha_value, params...) -> PK1 */ -template -SolidMechanicsWithInternalVarsSystem -buildSolidMechanicsWithInternalVarsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, InternalVarTimeRule state_rule, - std::string prepend_name = "", - std::shared_ptr cycle_zero_solver = nullptr, - FieldType... parameter_types) +template +void setCoupledSolidMechanicsInternalVariableMaterial( + std::shared_ptr> solid, + std::shared_ptr> /*internal_variables*/, + const MaterialType& material, const std::string& domain_name) { - auto field_store = std::make_shared(mesh, 100); - - auto prefix = [&](const std::string& name) { - if (prepend_name.empty()) { - return name; - } - return prepend_name + "_" + name; - }; - - // Add shape displacement - FieldType> shape_disp_type(prefix("shape_displacement")); - field_store->addShapeDisp(shape_disp_type); - - // 1. Displacement fields (4-state second-order) - auto disp_time_rule_ptr = std::make_shared(disp_rule); - FieldType> disp_type(prefix("displacement_solve_state")); - auto disp_bc = field_store->addIndependent(disp_type, disp_time_rule_ptr); - auto disp_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::VAL, prefix("displacement")); - auto velo_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DOT, prefix("velocity")); - auto accel_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DDOT, prefix("acceleration")); - - // 2. Internal variable fields (2-state first-order) - auto state_time_rule_ptr = std::make_shared(state_rule); - FieldType state_type(prefix("state_solve_state")); - auto state_bc = field_store->addIndependent(state_type, state_time_rule_ptr); - auto state_old_type = field_store->addDependent(state_type, FieldStore::TimeDerivative::VAL, prefix("state")); - - // 3. Parameters - std::vector parameter_fields; - (field_store->addParameter(FieldType(prefix("param_" + parameter_types.name))), ...); - (parameter_fields.push_back(field_store->getField(prefix("param_" + parameter_types.name))), ...); + auto solid_material = detail::CoupledSolidInternalVariableMaterialAdapter{material, material.density}; - using SystemType = SolidMechanicsWithInternalVarsSystem; - - // 4. Solid weak form: residual for u (inputs: u, u_old, v_old, a_old, alpha, alpha_old, params...) - std::string solid_res_name = prefix("solid_residual"); - auto solid_weak_form = std::make_shared( - solid_res_name, field_store->getMesh(), field_store->getField(disp_type.name).get()->space(), - field_store->createSpaces(solid_res_name, disp_type.name, disp_type, disp_old_type, velo_old_type, accel_old_type, - state_type, state_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); - - // 5. State weak form: residual for alpha (inputs: alpha, alpha_old, u, u_old, v_old, a_old, params...) - std::string state_res_name = prefix("state_residual"); - auto state_weak_form = std::make_shared( - state_res_name, field_store->getMesh(), field_store->getField(state_type.name).get()->space(), - field_store->createSpaces(state_res_name, state_type.name, state_type, state_old_type, disp_type, disp_old_type, - velo_old_type, accel_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); - - // 6. Cycle-zero weak form: solve for acceleration (inputs: u, v, a, alpha, params...) - std::string cycle_zero_name = prefix("solid_reaction"); - auto cycle_zero_solid_weak_form = std::make_shared( - cycle_zero_name, field_store->getMesh(), field_store->getField(accel_old_type.name).get()->space(), - field_store->createSpaces(cycle_zero_name, accel_old_type.name, disp_type, velo_old_type, accel_old_type, - state_type, FieldType(prefix("param_" + parameter_types.name))...)); - - if (cycle_zero_solver == nullptr) { - cycle_zero_solver = solver->singleBlockSolver(0); - } - SLIC_ERROR_IF(cycle_zero_solver == nullptr, - "Could not derive a cycle-zero solver for block 0 from the provided internal-vars solid mechanics " - "solver."); - - // Solver and Advancer - std::vector> weak_forms{solid_weak_form, state_weak_form}; - auto advancer = std::make_shared(field_store, weak_forms, solver, - cycle_zero_solid_weak_form, cycle_zero_solver); + solid->setMaterial(solid_material, domain_name); +} - return SystemType{{field_store, solver, advancer, parameter_fields, prepend_name}, - solid_weak_form, - state_weak_form, - cycle_zero_solid_weak_form, - disp_bc, - state_bc, - disp_time_rule_ptr, - state_time_rule_ptr}; +/** + * @brief Register an internal-variable evolution law using solid displacement coupling. + * + * Evolution callable may satisfy either: + * material(t_info, alpha, alpha_dot, grad_u, params...) -> residual + * or + * material(alpha, alpha_dot, grad_u, params...) -> residual + */ +template +void setCoupledInternalVariableMaterial( + std::shared_ptr> internal_variables, + std::shared_ptr> /*solid*/, + const MaterialType& material, const std::string& domain_name) +{ + internal_variables->addEvolution( + domain_name, [=](auto t_info, auto alpha, auto alpha_dot, auto u, auto /*v*/, auto /*a*/, auto... params) { + return detail::evaluateCoupledInternalVariableMaterial(material, t_info, alpha, alpha_dot, get(u), + params...); + }); } +template /** - * @brief Factory function to build a solid mechanics with internal vars system (without physics name). + * @brief Backward-compatible alias for `setCoupledSolidMechanicsInternalVariableMaterial`. + * @param solid Solid system receiving internal-variable coupling. + * @param state Backward-compatible internal-variable system alias. + * @param material Material model. + * @param domain_name Domain on which to apply the material. */ -template -auto buildSolidMechanicsWithInternalVarsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, InternalVarTimeRule state_rule, - std::shared_ptr cycle_zero_solver = nullptr, - FieldType... parameter_types) +void setCoupledSolidMechanicsInternalVarsMaterial( + std::shared_ptr> solid, + std::shared_ptr> state, + const MaterialType& material, const std::string& domain_name) { - return buildSolidMechanicsWithInternalVarsSystem(mesh, solver, disp_rule, state_rule, "", - cycle_zero_solver, parameter_types...); + setCoupledSolidMechanicsInternalVariableMaterial(solid, state, material, domain_name); } } // namespace smith diff --git a/src/smith/differentiable_numerics/state_variable_system.hpp b/src/smith/differentiable_numerics/state_variable_system.hpp new file mode 100644 index 0000000000..25c237d2de --- /dev/null +++ b/src/smith/differentiable_numerics/state_variable_system.hpp @@ -0,0 +1,253 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file state_variable_system.hpp + * @brief Standalone composable system for a first-order internal variable (damage, plasticity, etc.). + * + * Two-phase factory: + * auto internal_variable_fields = registerInternalVariableFields( + * field_store, params...); + * + * auto internal_variable_system = buildInternalVariableSystem( + * solver, internal_variable_fields, couplingFields(solid_fields), param_fields); + * + * The returned PhysicsFields from registerInternalVariableFields carries field tokens + * (state_solve_state, state) that can be injected into another physics system + * (e.g. SolidMechanicsSystem) as coupling input. + * + * addEvolution registers an ODE residual of the form: + * evolution_law(t_info, alpha_val, alpha_dot, interpolated_coupling_fields..., params...) == 0 + * + * With solid displacement coupling, the callback receives `(u, v, a)` rather than raw + * `(u_solve_state, u_old, v_old, a_old)`. + */ + +#pragma once + +#include "smith/differentiable_numerics/field_store.hpp" +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" +#include "smith/differentiable_numerics/state_advancer.hpp" +#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" +#include "smith/differentiable_numerics/time_integration_rule.hpp" +#include "smith/physics/functional_weak_form.hpp" +#include "smith/differentiable_numerics/differentiable_physics.hpp" +#include "smith/physics/weak_form.hpp" +#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/coupling_params.hpp" + +namespace smith { + +/** + * @brief System for a single internal variable using a two-state first-order rule. + * + * Field layout: (state_solve_state, state) - 2 fields. + * With a non-empty Coupling, coupling fields appear after the two state fields, + * before user parameter fields. + * + * @tparam dim Spatial dimension (needed for the weak form and zero-flux tensor). + * @tparam StateSpace FE space for the internal variable (e.g., L2<0>). + * @tparam InternalVarTimeRule Time integration rule (must have num_states == 2). + * @tparam Coupling Tuple of coupling and parameter packs (default: none). + */ +template > +struct InternalVariableSystem : public SystemBase { + using SystemBase::SystemBase; + + static_assert(InternalVarTimeRule::num_states == 2, + "InternalVariableSystem requires a 2-state time integration rule"); + + /// State weak form: (alpha, alpha_old, coupling_fields..., params...) + using InternalVariableWeakFormType = + FunctionalWeakForm>; + + std::shared_ptr internal_variable_weak_form; ///< Internal variable weak form. + std::shared_ptr internal_variable_bc; ///< Internal variable BCs. + std::shared_ptr internal_variable_time_rule; ///< Time integration rule. + std::shared_ptr coupling; ///< Coupling metadata. + + /** + * @brief Register an ODE evolution law for the internal variable. + * + * The evolution_law is called as: + * evolution_law(t_info, alpha_val, alpha_dot, coupling_fields..., params...) + * and must return a scalar residual (zero when the ODE is satisfied). + * + * @param domain_name Domain to apply the evolution on. + * @param evolution_law Callable returning the ODE residual. + */ + template + void addEvolution(const std::string& domain_name, EvolutionType evolution_law) + { + auto captured_time_rule = internal_variable_time_rule; + auto captured_coupling = coupling; + internal_variable_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_time_rule, *captured_coupling, t_info, + [&](auto alpha_current, auto alpha_dot, auto... interpolated_params) { + auto residual_val = + evolution_law(t_info, get(alpha_current), get(alpha_dot), interpolated_params...); + tensor flux{}; + return smith::tuple{residual_val, flux}; + }, + raw_args...); + }); + } +}; + +// --------------------------------------------------------------------------- +// Phase 1: registerInternalVariableFields +// --------------------------------------------------------------------------- + +/** + * @brief Register state variable fields into a FieldStore. + * + * Adds a 2-field layout: (state_solve_state, state). + * + * @return PhysicsFields carrying (state_solve_state, state) field tokens + * suitable for injection into another physics system. + */ +template +auto registerInternalVariableFields(std::shared_ptr field_store, + FieldType... parameter_types) +{ + auto internal_variable_time_rule = std::make_shared(); + FieldType state_type("state_solve_state"); + field_store->addIndependent(state_type, internal_variable_time_rule); + field_store->addDependent(state_type, FieldStore::TimeDerivative::VAL, "state"); + + if constexpr (sizeof...(parameter_space) > 0) { + auto prefix_param = [&](auto& pt) { + pt.name = "param_" + pt.name; + field_store->addParameter(pt); + }; + (prefix_param(parameter_types), ...); + } + + return PhysicsFields{ + field_store, FieldType(field_store->prefix("state_solve_state")), + FieldType(field_store->prefix("state"))}; +} + +template +/// @brief Backward-compatible alias for `registerInternalVariableFields`. +auto registerStateVariableFields(std::shared_ptr field_store, FieldType... parameter_types) +{ + return registerInternalVariableFields(field_store, parameter_types...); +} + +// --------------------------------------------------------------------------- +// Phase 2: buildInternalVariableSystem +// --------------------------------------------------------------------------- + +/** + * @brief Build an InternalVariableSystem with coupling, assuming fields are already registered. + */ +namespace detail { + +/** + * @brief Internal builder for an internal-variable system after public registration and coupling collection. + */ +template + requires detail::is_coupling_packs_v +auto buildInternalVariableSystemImpl(std::shared_ptr field_store, const Coupling& coupling, + std::shared_ptr solver) +{ + auto internal_variable_time_rule = std::make_shared(); + + FieldType state_type(field_store->prefix("state_solve_state"), true); + FieldType state_old_type(field_store->prefix("state")); + + auto internal_variable_bc = field_store->getBoundaryConditions(state_type.name); + + using SystemType = InternalVariableSystem; + + std::string internal_variable_residual_name = field_store->prefix("state_residual"); + auto internal_variable_weak_form = + detail::buildWeakFormWithCoupling( + field_store, internal_variable_residual_name, state_type.name, state_type, state_old_type, + detail::flattenCouplingFields(coupling)); + + auto sys = std::make_shared(field_store, solver, + std::vector>{internal_variable_weak_form}); + sys->internal_variable_bc = internal_variable_bc; + sys->internal_variable_time_rule = internal_variable_time_rule; + sys->coupling = std::make_shared(coupling); + sys->internal_variable_weak_form = internal_variable_weak_form; + + return sys; +} + +} // namespace detail + +/** + * @brief Build an internal-variable system from registered field packs. + * + * The time rule is deduced from SelfFields::time_rule_type. + */ +template + requires(detail::has_time_rule_v) +auto buildInternalVariableSystem(std::shared_ptr solver, const SelfFields& self_fields) +{ + constexpr int dim = SelfFields::dim; + using StateSpace = typename std::tuple_element_t<0, decltype(self_fields.fields)>::space_type; + using InternalVarTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(); + return detail::buildInternalVariableSystemImpl(field_store, coupling, solver); +} + +/** + * @brief Build an InternalVariableSystem from registered self fields plus coupled physics fields. + */ +template + requires(detail::has_time_rule_v) +auto buildInternalVariableSystem(std::shared_ptr solver, const SelfFields& self_fields, + const CouplingFields& coupled) +{ + constexpr int dim = SelfFields::dim; + using StateSpace = typename std::tuple_element_t<0, decltype(self_fields.fields)>::space_type; + using InternalVarTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled); + return detail::buildInternalVariableSystemImpl(field_store, coupling, solver); +} + +/** + * @brief Build an InternalVariableSystem from registered self fields plus registered parameter fields. + */ +template + requires(detail::has_time_rule_v) +auto buildInternalVariableSystem(std::shared_ptr solver, const SelfFields& self_fields, + const ParamFields& params) +{ + constexpr int dim = SelfFields::dim; + using StateSpace = typename std::tuple_element_t<0, decltype(self_fields.fields)>::space_type; + using InternalVarTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(params); + return detail::buildInternalVariableSystemImpl(field_store, coupling, solver); +} + +/** + * @brief Build an InternalVariableSystem from registered self fields, coupled physics fields, and parameter fields. + */ +template + requires(detail::has_time_rule_v) +auto buildInternalVariableSystem(std::shared_ptr solver, const SelfFields& self_fields, + const CouplingFields& coupled, const ParamFields& params) +{ + constexpr int dim = SelfFields::dim; + using StateSpace = typename std::tuple_element_t<0, decltype(self_fields.fields)>::space_type; + using InternalVarTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled, params); + return detail::buildInternalVariableSystemImpl(field_store, coupling, solver); +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/system_base.cpp b/src/smith/differentiable_numerics/system_base.cpp new file mode 100644 index 0000000000..e13589eb90 --- /dev/null +++ b/src/smith/differentiable_numerics/system_base.cpp @@ -0,0 +1,89 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/reaction.hpp" +#include "smith/differentiable_numerics/nonlinear_solve.hpp" + +namespace smith { + +std::vector SystemBase::solve(const TimeInfo& time_info) const +{ + std::vector weak_form_names; + for (const auto& wf : weak_forms) { + weak_form_names.push_back(wf->name()); + } + std::vector> index_map = field_store->indexMap(weak_form_names); + + std::vector> inputs; + if (!solve_input_field_names.empty()) { + SLIC_ERROR_IF(solve_input_field_names.size() != weak_forms.size(), + "solve_input_field_names size must match weak_forms size"); + } + for (size_t i = 0; i < weak_forms.size(); ++i) { + std::vector fields_for_wk; + if (solve_input_field_names.empty()) { + fields_for_wk = field_store->getStates(weak_forms[i]->name()); + } else { + for (const auto& field_name : solve_input_field_names[i]) { + fields_for_wk.push_back(field_store->getField(field_name)); + } + } + inputs.push_back(fields_for_wk); + } + + std::vector bc_field_names; + if (!solve_result_field_names.empty()) { + SLIC_ERROR_IF(solve_result_field_names.size() != weak_forms.size(), + "solve_result_field_names size must match weak_forms size"); + bc_field_names = solve_result_field_names; + for (size_t row = 0; row < weak_forms.size(); ++row) { + for (size_t col = 0; col < weak_forms.size(); ++col) { + index_map[row][col] = invalid_block_index; + for (size_t arg = 0; arg < inputs[row].size(); ++arg) { + if (inputs[row][arg].get()->name() == solve_result_field_names[col]) { + index_map[row][col] = arg; + break; + } + } + } + SLIC_ERROR_IF(index_map[row][row] == invalid_block_index, "Requested solve result field '" + << solve_result_field_names[row] + << "' is not an argument of weak form '" + << weak_form_names[row] << "'"); + } + } + + auto params = field_store->getParameterFields(); + std::vector> wk_params(weak_forms.size(), params); + + std::vector weak_form_ptrs; + for (auto& p : weak_forms) { + weak_form_ptrs.push_back(p.get()); + } + auto bc_managers = solve_result_field_names.empty() + ? field_store->getBoundaryConditionManagers(weak_form_names) + : field_store->getBoundaryConditionManagersForFields(bc_field_names); + return solver->solve(weak_form_ptrs, index_map, field_store->getShapeDisp(), inputs, wk_params, time_info, + bc_managers); +} + +std::vector SystemBase::computeReactions(const TimeInfo& time_info, + const std::vector& states_for_reactions) const +{ + std::vector reactions; + auto params = field_store->getParameterFields(); + for (const auto& wf : weak_forms) { + std::vector wf_fields = field_store->getStatesFromVectors(wf->name(), states_for_reactions, params); + std::string test_field_name = field_store->getWeakFormReaction(wf->name()); + size_t test_field_idx = field_store->getFieldIndex(test_field_name); + FieldState test_field = states_for_reactions[test_field_idx]; + reactions.push_back(smith::evaluateWeakForm(wf, time_info, field_store->getShapeDisp(), wf_fields, test_field)); + } + return reactions; +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/system_base.hpp b/src/smith/differentiable_numerics/system_base.hpp index c6b572a450..4a4d9e7ec1 100644 --- a/src/smith/differentiable_numerics/system_base.hpp +++ b/src/smith/differentiable_numerics/system_base.hpp @@ -17,7 +17,7 @@ #include #include "field_state.hpp" #include "field_store.hpp" -#include "coupled_system_solver.hpp" +#include "system_solver.hpp" #include "state_advancer.hpp" #include "smith/physics/common.hpp" #include "mfem.hpp" @@ -27,14 +27,6 @@ namespace smith { template struct Parameters; -/** - * @brief Information about a dual field. - */ -struct ReactionInfo { - std::string name; ///< The name of the dual field. - const mfem::ParFiniteElementSpace* space = nullptr; ///< The finite element space of the dual field. -}; - namespace detail { /// @brief Helper: given an index and a type, always produces the type (used to repeat a type N times via pack @@ -58,33 +50,93 @@ using TimeRuleParams = * @brief Base struct for physics systems containing common members and helper functions. */ struct SystemBase { - std::shared_ptr field_store; ///< Field store managing the system's fields. - std::shared_ptr solver; ///< The solver for the system. - std::shared_ptr advancer; ///< The state advancer. - std::vector parameter_fields; ///< Optional parameter fields. - std::string prepend_name; ///< Optional prepended name for all fields. - + // --- equations --- + std::vector> weak_forms; ///< Weak forms solved together by this system. + + // --- infrastructure --- + std::shared_ptr field_store; ///< Field store managing the system's fields. + std::shared_ptr solver; ///< The solver for the system. + std::vector> + cycle_zero_systems; ///< Optional startup solves executed before first timestep. Each entry is solved + ///< independently; cycle-zero solves do not couple across subsystems. + std::vector> post_solve_systems; ///< Optional systems solved after main state update. + std::vector + solve_result_field_names; ///< Optional per-weak-form fields to solve/update instead of reaction fields. + std::vector> + solve_input_field_names; ///< Optional per-weak-form input field ordering used during solve. + + /// @brief Construct an empty system shell. + SystemBase() = default; /** - * @brief Get the list of all parameter fields. - * @return const std::vector& List of parameter fields. + * @brief Construct a system from a field store, solver, and weak forms. + * @param fs Field store shared by all weak forms. + * @param sol Solver used for `solve`. + * @param wfs Weak forms owned by this system. */ - const std::vector& getParameterFields() const { return parameter_fields; } + explicit SystemBase(std::shared_ptr fs, std::shared_ptr sol = nullptr, + std::vector> wfs = {}) + : weak_forms(std::move(wfs)), field_store(std::move(fs)), solver(std::move(sol)) + { + } + virtual ~SystemBase() = default; /** - * @brief Helper function to prepend the physics name to a string. - * @param name The name to prepend to. - * @return std::string The prepended name. + * @brief Solve the system using the internal weak_forms and solver. + * @param time_info Current time information. + * @return std::vector The updated state fields from the solver. */ - std::string prefix(const std::string& name) const - { - if (prepend_name.empty()) { - return name; - } - return prepend_name + "_" + name; - } + virtual std::vector solve(const TimeInfo& time_info) const; - /// @brief Metadata for dual outputs exported by this system. - std::vector getReactionInfos() const { return {}; } + /** + * @brief Compute reactions after solving the main state. + * @param time_info Current time information. + * @param states_for_reactions The fields configured for reaction computation. + * @return std::vector Computed reactions across all weak_forms. + */ + virtual std::vector computeReactions(const TimeInfo& time_info, + const std::vector& states_for_reactions) const; }; +/** + * @brief Convenience factory for a plain `SystemBase`. + */ +inline std::shared_ptr makeSystem(std::shared_ptr field_store, + std::shared_ptr solver, + std::vector> weak_forms) +{ + return std::make_shared(std::move(field_store), std::move(solver), std::move(weak_forms)); +} + +} // namespace smith + +namespace smith { +namespace detail { + +/// @brief Expands coupling tuple into trailing weak-form space arguments. +template +auto buildWeakFormWithCouplingImpl(const FieldStorePtr& field_store, const std::string& weak_form_name, + const std::string& unknown_field_name, const std::tuple& args_tuple, + std::index_sequence) +{ + auto coupling_fields_tuple = std::get(args_tuple); + return std::apply( + [&](const auto&... coupling_fields) { + return std::make_shared(weak_form_name, field_store->getMesh(), + field_store->getField(unknown_field_name).get()->space(), + field_store->createSpaces(weak_form_name, unknown_field_name, + std::get(args_tuple)..., coupling_fields...)); + }, + coupling_fields_tuple); +} + +/// @brief Builds weak form using regular args plus final coupling pack argument. +template +auto buildWeakFormWithCoupling(const FieldStorePtr& field_store, const std::string& weak_form_name, + const std::string& unknown_field_name, const Args&... args) +{ + return buildWeakFormWithCouplingImpl(field_store, weak_form_name, unknown_field_name, std::tie(args...), + std::make_index_sequence{}); +} + +} // namespace detail } // namespace smith diff --git a/src/smith/differentiable_numerics/coupled_system_solver.cpp b/src/smith/differentiable_numerics/system_solver.cpp similarity index 68% rename from src/smith/differentiable_numerics/coupled_system_solver.cpp rename to src/smith/differentiable_numerics/system_solver.cpp index 85599fa745..5d234a8c20 100644 --- a/src/smith/differentiable_numerics/coupled_system_solver.cpp +++ b/src/smith/differentiable_numerics/system_solver.cpp @@ -4,7 +4,7 @@ // // SPDX-License-Identifier: (BSD-3-Clause) -#include "smith/differentiable_numerics/coupled_system_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" #include "smith/differentiable_numerics/nonlinear_solve.hpp" #include "smith/physics/weak_form.hpp" @@ -13,40 +13,66 @@ #include #include +#include +#include #include #include namespace smith { -CoupledSystemSolver::CoupledSystemSolver(std::shared_ptr single_solver) +SystemSolver::SystemSolver(std::shared_ptr single_solver) : max_staggered_iterations_(1), exact_staggered_steps_(false) { addSubsystemSolver({}, std::move(single_solver)); } -CoupledSystemSolver::CoupledSystemSolver(int max_staggered_iterations, bool exact_staggered_steps) +SystemSolver::SystemSolver(int max_staggered_iterations, bool exact_staggered_steps) : max_staggered_iterations_(max_staggered_iterations), exact_staggered_steps_(exact_staggered_steps) { SLIC_ERROR_IF(max_staggered_iterations <= 0, "max_staggered_iterations must be > 0"); } -void CoupledSystemSolver::addSubsystemSolver(const std::vector& block_indices, - std::shared_ptr solver, double relaxation_factor) +void SystemSolver::addSubsystemSolver(const std::vector& block_indices, + std::shared_ptr solver, double relaxation_factor) { - SLIC_ERROR_IF(!solver, "CoupledSystemSolver stage solver must be non-null"); + SLIC_ERROR_IF(!solver, "SystemSolver stage solver must be non-null"); SLIC_ERROR_IF(relaxation_factor <= 0.0 || relaxation_factor > 1.0, axom::fmt::format("Stage relaxation_factor {} must be in (0, 1]", relaxation_factor)); stages_.push_back(Stage{block_indices, std::move(solver), relaxation_factor}); } -std::vector CoupledSystemSolver::solve( - const std::vector& residual_evals, const std::vector>& block_indices, - const FieldState& shape_disp, const std::vector>& states, - const std::vector>& params, const TimeInfo& time_info, - const std::vector& bc_managers) const +void SystemSolver::appendStagesWithBlockMapping(const SystemSolver& subsystem_solver, + const std::vector& global_block_indices) { - SLIC_ERROR_IF(stages_.empty(), "CoupledSystemSolver has no stages defined."); + SLIC_ERROR_IF(global_block_indices.empty(), "Global block index map must be non-empty"); + + for (const auto& stage : subsystem_solver.stages_) { + std::vector remapped_block_indices; + if (stage.block_indices.empty()) { + remapped_block_indices = global_block_indices; + } else { + remapped_block_indices.reserve(stage.block_indices.size()); + for (size_t local_block_index : stage.block_indices) { + SLIC_ERROR_IF(local_block_index >= global_block_indices.size(), + axom::fmt::format("Local block index {} exceeds subsystem size {}", local_block_index, + global_block_indices.size())); + remapped_block_indices.push_back(global_block_indices[local_block_index]); + } + } + addSubsystemSolver(remapped_block_indices, stage.solver, stage.relaxation_factor); + } +} + +std::vector SystemSolver::solve(const std::vector& residual_evals, + const std::vector>& block_indices, + const FieldState& shape_disp, + const std::vector>& states, + const std::vector>& params, + const TimeInfo& time_info, + const std::vector& bc_managers) const +{ + SLIC_ERROR_IF(stages_.empty(), "SystemSolver has no stages defined."); size_t num_residuals = residual_evals.size(); std::vector active_stages = stages_; @@ -55,6 +81,10 @@ std::vector CoupledSystemSolver::solve( stage.block_indices.resize(num_residuals); std::iota(stage.block_indices.begin(), stage.block_indices.end(), 0); } + for (size_t block_index : stage.block_indices) { + SLIC_ERROR_IF(block_index >= num_residuals, + axom::fmt::format("Stage block index {} exceeds residual count {}", block_index, num_residuals)); + } } // Set the inner tolerance factor based on the number of stages. For single-stage // solves, we don't want to reduce the tolerances as that's pointless and @@ -74,6 +104,16 @@ std::vector CoupledSystemSolver::solve( // Working copy of states, updated in-place as stages solve std::vector> current_states = states; + // Pre-compute name -> (row, slot) routing so the propagation loop avoids O(N*M) string compares + // on every staggered iteration. Field-name identity within current_states is invariant across + // the iteration loop: only values are replaced, never the underlying name. + std::unordered_map>> field_routing; + for (size_t r = 0; r < num_residuals; ++r) { + for (size_t slot = 0; slot < current_states[r].size(); ++slot) { + field_routing[current_states[r][slot].get()->name()].emplace_back(r, slot); + } + } + // Helper lambda to assemble input pointers, evaluate residual, and zero essential BCs auto eval_residual_and_zero_bcs = [&](size_t global_row) { std::vector input_ptrs; @@ -132,7 +172,9 @@ std::vector CoupledSystemSolver::solve( block_solve(stage_residuals, stage_block_indices, shape_disp, stage_states, stage_params, time_info, stage.solver.get(), stage_bc_managers); - // Propagate updated fields to all residuals that reference them. + // Propagate updated fields to every residual input that references the solved field. + // Match by field name (looked up via the pre-computed routing map): coupling fields appear + // as fixed inputs in other rows and therefore do not have a valid unknown-block entry there. // Apply relaxation: x_new = omega * x_solved + (1 - omega) * x_k. for (size_t i = 0; i < num_stage_blocks; ++i) { size_t global_col = stage.block_indices[i]; @@ -143,10 +185,10 @@ std::vector CoupledSystemSolver::solve( new_state = weighted_average(new_state, old_state, stage.relaxation_factor); } - for (size_t r = 0; r < num_residuals; ++r) { - size_t c = block_indices[r][global_col]; - if (c != invalid_block_index) { - current_states[r][c] = new_state; + auto it = field_routing.find(new_state.get()->name()); + if (it != field_routing.end()) { + for (const auto& [r, slot] : it->second) { + current_states[r][slot] = new_state; } } } @@ -188,12 +230,12 @@ std::vector CoupledSystemSolver::solve( return final_solutions; } -std::shared_ptr CoupledSystemSolver::singleBlockSolver(size_t block_index) const +std::shared_ptr SystemSolver::singleBlockSolver(size_t block_index) const { constexpr bool exact_staggered_steps = true; for (const auto& stage : stages_) { if (stage.block_indices.empty()) { - auto result = std::make_shared(1, exact_staggered_steps); + auto result = std::make_shared(1, exact_staggered_steps); std::shared_ptr stage_solver = stage.solver; if (const auto* equation_solver = dynamic_cast(stage.solver.get())) { if (auto cloned_solver = equation_solver->cloneFresh()) { @@ -207,7 +249,7 @@ std::shared_ptr CoupledSystemSolver::singleBlockSolver(size auto found = std::find(stage.block_indices.begin(), stage.block_indices.end(), block_index); if (found != stage.block_indices.end()) { - auto result = std::make_shared(1, exact_staggered_steps); + auto result = std::make_shared(1, exact_staggered_steps); std::shared_ptr stage_solver = stage.solver; if (const auto* equation_solver = dynamic_cast(stage.solver.get())) { if (auto cloned_solver = equation_solver->cloneFresh()) { diff --git a/src/smith/differentiable_numerics/coupled_system_solver.hpp b/src/smith/differentiable_numerics/system_solver.hpp similarity index 76% rename from src/smith/differentiable_numerics/coupled_system_solver.hpp rename to src/smith/differentiable_numerics/system_solver.hpp index d0d6914f52..71351f1c53 100644 --- a/src/smith/differentiable_numerics/coupled_system_solver.hpp +++ b/src/smith/differentiable_numerics/system_solver.hpp @@ -20,7 +20,7 @@ class NonlinearBlockSolverBase; class BoundaryConditionManager; /// @brief Orchestrates staggered solution for multiphysics systems. -class CoupledSystemSolver { +class SystemSolver { public: /// @brief Represents a single stage in a staggered iteration. struct Stage { @@ -31,18 +31,18 @@ class CoupledSystemSolver { ///< A value of 1.0 (default) means no relaxation (full update). }; - /// @brief Construct a monolithic CoupledSystemSolver from a single block solver. + /// @brief Construct a monolithic SystemSolver from a single block solver. /// @param single_solver The solver to use for all blocks simultaneously. - CoupledSystemSolver(std::shared_ptr single_solver); + SystemSolver(std::shared_ptr single_solver); - /// @brief Construct a CoupledSystemSolver for staggered iteration. + /// @brief Construct a SystemSolver for staggered iteration. /// @param max_staggered_iterations Maximum number of staggered sweeps across all stages. When /// @p exact_staggered_steps is false, the solver may exit early once all stage solvers /// report convergence. /// @param exact_staggered_steps If true, always perform exactly @p max_staggered_iterations /// sweeps with no early-exit convergence check. Useful when a fixed number of /// partitioned-stagger steps is required regardless of residual level. - CoupledSystemSolver(int max_staggered_iterations, bool exact_staggered_steps = false); + SystemSolver(int max_staggered_iterations, bool exact_staggered_steps = false); /// @brief Convenience method to add a solver stage. /// @param block_indices Indices of the blocks to solve. @@ -51,6 +51,12 @@ class CoupledSystemSolver { void addSubsystemSolver(const std::vector& block_indices, std::shared_ptr solver, double relaxation_factor = 1.0); + /// @brief Append stages from another solver using a local-to-global block mapping. + /// @param subsystem_solver Source solver whose stages operate on subsystem-local block indices. + /// @param global_block_indices Mapping from subsystem-local block index to global block index. + void appendStagesWithBlockMapping(const SystemSolver& subsystem_solver, + const std::vector& global_block_indices); + /// @brief Solves the multiphysics system using staggered iterations. /// @param residual_evals Vector of WeakForm evaluations for each block. /// @param block_indices Block indices for each residual evaluation. @@ -68,7 +74,13 @@ class CoupledSystemSolver { /// @brief Build a single-block solver from the stage responsible for @p block_index. /// Prefers constructing a fresh solver instance when the underlying stage solver retains rebuildable config. - std::shared_ptr singleBlockSolver(size_t block_index) const; + std::shared_ptr singleBlockSolver(size_t block_index) const; + + /// @brief Maximum number of staggered sweeps allowed for this solver. + int maxStaggeredIterations() const { return max_staggered_iterations_; } + + /// @brief Whether solver always performs exactly `maxStaggeredIterations()` sweeps. + bool exactStaggeredSteps() const { return exact_staggered_steps_; } private: int max_staggered_iterations_; ///< Maximum number of staggered iterations. diff --git a/src/smith/differentiable_numerics/tests/CMakeLists.txt b/src/smith/differentiable_numerics/tests/CMakeLists.txt index bbe95cbd55..28a97dc795 100644 --- a/src/smith/differentiable_numerics/tests/CMakeLists.txt +++ b/src/smith/differentiable_numerics/tests/CMakeLists.txt @@ -9,13 +9,15 @@ set(differentiable_numerics_test_depends smith_physics smith_differentiable_nume set(differentiable_numerics_test_source test_solver_convergence.cpp test_field_state.cpp + test_make_time_info_material.cpp test_solid_dynamics.cpp test_explicit_dynamics.cpp test_porous_heat_sink.cpp - test_thermo_mechanics.cpp + test_combined_thermo_mechanics.cpp test_thermal_static.cpp test_solid_static_with_internal_vars.cpp test_multiphysics_time_integrator.cpp + test_thermo_mechanics_with_internal_vars.cpp ) smith_add_tests( SOURCES ${differentiable_numerics_test_source} diff --git a/src/smith/differentiable_numerics/tests/test_combined_thermo_mechanics.cpp b/src/smith/differentiable_numerics/tests/test_combined_thermo_mechanics.cpp new file mode 100644 index 0000000000..856d5e42c0 --- /dev/null +++ b/src/smith/differentiable_numerics/tests/test_combined_thermo_mechanics.cpp @@ -0,0 +1,527 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include +#include +#include "gtest/gtest.h" + +#include "smith/smith_config.hpp" +#include "smith/infrastructure/application_manager.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/physics/state/state_manager.hpp" +#include "smith/physics/mesh.hpp" +#include "smith/physics/materials/green_saint_venant_thermoelastic.hpp" +#include "smith/physics/materials/solid_material.hpp" + +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_system.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" +#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" +#include "smith/differentiable_numerics/differentiable_test_utils.hpp" +#include "smith/differentiable_numerics/evaluate_objective.hpp" +#include "smith/differentiable_numerics/nonlinear_solve.hpp" +#include "smith/differentiable_numerics/make_time_info_material.hpp" +#include "smith/physics/functional_objective.hpp" +#include "gretl/wang_checkpoint_strategy.hpp" + +namespace smith { + +static constexpr int dim = 3; +static constexpr int displacement_order = 1; +static constexpr int temperature_order = 1; + +using DispRule = QuasiStaticSecondOrderTimeIntegrationRule; +using TemperatureRule = BackwardEulerFirstOrderTimeIntegrationRule; + +TEST(CouplingTimeRuleInterpolation, AppliesEachForeignPhysicsRuleBeforeCallback) +{ + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "dcoupling_interpolation"); + auto mesh = + std::make_shared(mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON), "mesh", 0, 0); + auto field_store = std::make_shared(mesh, 100, ""); + auto solid_fields = registerSolidMechanicsFields(field_store); + auto thermal_fields = registerThermalFields(field_store); + auto scale_params = registerParameterFields(field_store, FieldType>("scale")); + + TemperatureRule self_rule; + auto solid_coupling = detail::collectCouplingFields(couplingFields(thermal_fields), scale_params); + auto saw_thermal_values = detail::applyTimeRuleAndCoupling( + self_rule, solid_coupling, TimeInfo(0.0, 2.0, 0), + [](auto self_value, auto self_dot, auto temperature, auto temperature_dot, auto scale) { + EXPECT_DOUBLE_EQ(self_value, 9.0); + EXPECT_DOUBLE_EQ(self_dot, 2.0); + EXPECT_DOUBLE_EQ(temperature, 7.0); + EXPECT_DOUBLE_EQ(temperature_dot, 3.0); + EXPECT_DOUBLE_EQ(scale, 11.0); + return true; + }, + 9.0, 5.0, 7.0, 1.0, 11.0); + EXPECT_TRUE(saw_thermal_values); + + auto thermal_coupling = detail::collectCouplingFields(couplingFields(solid_fields)); + auto saw_solid_values = detail::applyTimeRuleAndCoupling( + self_rule, thermal_coupling, TimeInfo(0.0, 2.0, 0), + [](auto self_value, auto self_dot, auto displacement, auto velocity, auto /*acceleration*/) { + EXPECT_DOUBLE_EQ(self_value, 12.0); + EXPECT_DOUBLE_EQ(self_dot, 2.0); + EXPECT_DOUBLE_EQ(displacement, 10.0); + EXPECT_DOUBLE_EQ(velocity, 3.0); + return true; + }, + 12.0, 8.0, 10.0, 4.0, 0.0, 0.0); + EXPECT_TRUE(saw_solid_values); +} + +TEST(ParameterFieldsRegistration, RegistersImmediatelyAndReturnsPrefixedFields) +{ + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "parameter_registration"); + auto mesh = + std::make_shared(mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON), "mesh", 0, 0); + auto field_store = std::make_shared(mesh, 100, "coupled"); + + auto param_fields = registerParameterFields(field_store, FieldType>("scale"), FieldType>("offset")); + + static_assert(std::is_same_v, ParamFields, L2<0>>>); + EXPECT_EQ(field_store->getParameterFields().size(), 2); + EXPECT_EQ(std::get<0>(param_fields.fields).name, field_store->prefix("param_scale")); + EXPECT_EQ(std::get<1>(param_fields.fields).name, field_store->prefix("param_offset")); + EXPECT_EQ(field_store->getParameterFields()[0].get()->name(), field_store->prefix("param_scale")); + EXPECT_EQ(field_store->getParameterFields()[1].get()->name(), field_store->prefix("param_offset")); +} + +TEST(CouplingTimeRuleInterpolation, PreservesForeignPacksWithSameTimeRuleType) +{ + axom::sidre::DataStore datastore; + smith::StateManager::initialize(datastore, "same_rule_coupling"); + auto mesh = + std::make_shared(mfem::Mesh::MakeCartesian3D(1, 1, 1, mfem::Element::HEXAHEDRON), "mesh", 0, 0); + auto field_store = std::make_shared(mesh, 100, ""); + + using ScalarSpace = H1; + PhysicsFields thermal_a{ + field_store, FieldType("temperature_a_solve_state"), FieldType("temperature_a")}; + PhysicsFields thermal_b{ + field_store, FieldType("temperature_b_solve_state"), FieldType("temperature_b")}; + + auto same_rule_coupling = detail::collectCouplingFields(couplingFields(thermal_a, thermal_b)); + TemperatureRule self_rule; + auto saw_all_values = detail::applyTimeRuleAndCoupling( + self_rule, same_rule_coupling, TimeInfo(0.0, 2.0, 0), + [](auto self_value, auto self_dot, auto temperature_a, auto temperature_a_dot, auto temperature_b, + auto temperature_b_dot) { + EXPECT_DOUBLE_EQ(self_value, 11.0); + EXPECT_DOUBLE_EQ(self_dot, 1.0); + EXPECT_DOUBLE_EQ(temperature_a, 7.0); + EXPECT_DOUBLE_EQ(temperature_a_dot, 3.0); + EXPECT_DOUBLE_EQ(temperature_b, 5.0); + EXPECT_DOUBLE_EQ(temperature_b_dot, 1.75); + return true; + }, + 11.0, 9.0, 7.0, 1.0, 5.0, 1.5); + EXPECT_TRUE(saw_all_values); +} + +struct ThermoMechanicsMeshFixture : public testing::Test { + void SetUp() + { + datastore_ = std::make_unique(); + smith::StateManager::initialize(*datastore_, "solid"); + mesh_ = std::make_shared( + mfem::Mesh::MakeCartesian3D(24, 2, 2, mfem::Element::HEXAHEDRON, 1.2, 0.03, 0.03), "mesh", 0, 0); + mesh_->addDomainOfBoundaryElements("left", smith::by_attr(3)); + mesh_->addDomainOfBoundaryElements("right", smith::by_attr(5)); + field_store_ = std::make_shared(mesh_, 100, ""); + } + + std::shared_ptr makeSolver(const NonlinearSolverOptions& nonlin, const LinearSolverOptions& lin) + { + return std::make_shared(buildNonlinearBlockSolver(nonlin, lin, *mesh_)); + } + + // Advance one step, return final states + lateral deflection. + template + double advanceOneStepAndGetLateralDeflection(std::shared_ptr coupled_system, double dt = 1.0) + { + auto shape_disp = field_store_->getShapeDisp(); + auto params = field_store_->getParameterFields(); + std::vector reactions; + std::tie(std::ignore, reactions) = + makeAdvancer(coupled_system) + ->advanceState(smith::TimeInfo(0.0, dt, 0), shape_disp, field_store_->getStateFields(), params); + + mfem::Vector final_disp(*field_store_->getField("displacement").get()); + double deflection = 0.0; + for (int i = 1; i < final_disp.Size(); i += dim) { + deflection = std::max(deflection, std::abs(final_disp(i))); + } + return deflection; + } + + template + void applyBucklingLoads(Solid& solid, Thermal& thermal, double compressive_traction, double lateral_body_force, + double thermal_source) + { + solid->setDisplacementBC(mesh_->domain("left")); + thermal->setTemperatureBC(mesh_->domain("left")); + thermal->setTemperatureBC(mesh_->domain("right")); + + solid->addTraction("right", [=](auto, auto X, auto, auto, auto, auto, auto... /*args*/) { + auto traction = 0.0 * X; + traction[0] = -compressive_traction; + return traction; + }); + solid->addBodyForce(mesh_->entireBodyName(), [=](auto, auto X, auto, auto, auto, auto, auto... /*args*/) { + auto force = 0.0 * X; + force[1] = lateral_body_force; + return force; + }); + thermal->addHeatSource(mesh_->entireBodyName(), [=](auto, auto, auto, auto... /*args*/) { return thermal_source; }); + } + + std::unique_ptr datastore_; + std::shared_ptr mesh_; + std::shared_ptr field_store_; +}; + +// Defaults used by multiple tests. +static const LinearSolverOptions directLinOpts{.linear_solver = LinearSolver::SuperLU}; +static const NonlinearSolverOptions newtonNonlinOpts{ + .nonlin_solver = NonlinearSolver::Newton, .relative_tol = 1e-10, .absolute_tol = 1e-10, .max_iterations = 4}; + +// 1. CreateDifferentiablePhysicsAllocatesReactionInfo +TEST_F(ThermoMechanicsMeshFixture, CreateDifferentiablePhysicsAllocatesReactionInfo) +{ + FieldType> thermal_expansion_scaling("thermal_expansion_scaling"); + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + auto param_fields = registerParameterFields(field_store_, thermal_expansion_scaling); + + auto solid_system = buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), SolidMechanicsOptions{}, + solid_fields, couplingFields(thermal_fields), param_fields); + + auto thermal_system = buildThermalSystem(makeSolver(newtonNonlinOpts, directLinOpts), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields), param_fields); + + auto coupled_system = combineSystems(solid_system, thermal_system); + auto physics = makeDifferentiablePhysics(coupled_system, "coupled_physics"); + const auto& solid_dual_space = physics->dual("reactions").space(); + const auto& solid_state_space = physics->state("displacement").space(); + const auto& thermal_dual_space = physics->dual("thermal_flux").space(); + const auto& thermal_state_space = physics->state("temperature").space(); + + EXPECT_EQ(physics->dualNames().size(), 2); + EXPECT_EQ(physics->dualNames()[0], "reactions"); + EXPECT_EQ(physics->dualNames()[1], "thermal_flux"); + EXPECT_EQ(solid_dual_space.GetMesh(), solid_state_space.GetMesh()); + EXPECT_STREQ(solid_dual_space.FEColl()->Name(), solid_state_space.FEColl()->Name()); + EXPECT_EQ(solid_dual_space.GetVDim(), solid_state_space.GetVDim()); + EXPECT_EQ(solid_dual_space.TrueVSize(), solid_state_space.TrueVSize()); + EXPECT_EQ(thermal_dual_space.GetMesh(), thermal_state_space.GetMesh()); + EXPECT_STREQ(thermal_dual_space.FEColl()->Name(), thermal_state_space.FEColl()->Name()); + EXPECT_EQ(thermal_dual_space.GetVDim(), thermal_state_space.GetVDim()); + EXPECT_EQ(thermal_dual_space.TrueVSize(), thermal_state_space.TrueVSize()); +} + +// 2. BackpropagateThroughPhysics +TEST_F(ThermoMechanicsMeshFixture, BackpropagateThroughPhysics) +{ + FieldType> youngs_modulus("youngs_modulus"); + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + auto param_fields = registerParameterFields(field_store_, youngs_modulus); + + auto solid_system = buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), SolidMechanicsOptions{}, + solid_fields, couplingFields(thermal_fields), param_fields); + + auto thermal_system = buildThermalSystem(makeSolver(newtonNonlinOpts, directLinOpts), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields), param_fields); + + auto coupled_system = combineSystems(solid_system, thermal_system); + auto material = makeTimeInfoMaterial( + thermomechanics::ParameterizedGreenSaintVenantThermoelasticMaterial{1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh_->entireBodyName()); + + coupled_system->field_store->getParameterFields()[0].get()->setFromFieldFunction( + [=](smith::tensor) { return 1.0; }); + + solid_system->setDisplacementBC(mesh_->domain("left")); + thermal_system->setTemperatureBC(mesh_->domain("left"), [](auto, auto) { return 1.0; }); + + solid_system->addTraction("right", [=](double, auto X, auto, auto, auto, auto, auto... /*params*/) { + // If X is a dual number, we need to create a dual number for traction with zero derivative wrt all active + // parameters. For now, returning a value works perfectly fine with smith AD system! But since X might be a dual + // number, we must strip its dual part if we just want a value. + auto traction = 0.0 * X; + traction[0] = -0.015; + return traction; + }); + + auto physics = makeDifferentiablePhysics(coupled_system, "coupled_physics"); + + double dt = 1.0; + for (int step = 0; step < 2; ++step) { + physics->advanceTimestep(dt); + } + + auto reactions = physics->getReactionStates(); + auto obj = 0.5 * (innerProduct(reactions[0], reactions[0]) + innerProduct(reactions[1], reactions[1])); + + gretl::set_as_objective(obj); + obj.data_store().back_prop(); + + auto param_sens = coupled_system->field_store->getParameterFields()[0].get_dual(); + EXPECT_TRUE(param_sens->Norml2() > 0.0); +} + +TEST_F(ThermoMechanicsMeshFixture, BackpropagateThroughStaggeredPhysics) +{ + FieldType> thermal_expansion_scaling("thermal_expansion_scaling"); + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + auto param_fields = registerParameterFields(field_store_, thermal_expansion_scaling); + + LinearSolverOptions solid_lin_opts{.linear_solver = LinearSolver::CG, + .preconditioner = Preconditioner::HypreAMG, + .relative_tol = 1e-6, + .absolute_tol = 1e-10, + .max_iterations = 120}; + NonlinearSolverOptions solid_nonlin_opts{ + .nonlin_solver = NonlinearSolver::TrustRegion, .relative_tol = 1e-6, .absolute_tol = 1e-7, .max_iterations = 25}; + LinearSolverOptions thermal_lin_opts{ + .linear_solver = LinearSolver::SuperLU, .relative_tol = 1e-8, .absolute_tol = 1e-10, .max_iterations = 80}; + NonlinearSolverOptions thermal_nonlin_opts{.nonlin_solver = NonlinearSolver::NewtonLineSearch, + .relative_tol = 1e-7, + .absolute_tol = 1e-8, + .max_iterations = 12, + .max_line_search_iterations = 6}; + + auto solid_system = buildSolidMechanicsSystem(nullptr, SolidMechanicsOptions{}, solid_fields, + couplingFields(thermal_fields), param_fields); + auto thermal_system = + buildThermalSystem(nullptr, ThermalOptions{}, thermal_fields, couplingFields(solid_fields), param_fields); + + auto coupled_solver = std::make_shared(10); + coupled_solver->addSubsystemSolver({0}, buildNonlinearBlockSolver(solid_nonlin_opts, solid_lin_opts, *mesh_)); + coupled_solver->addSubsystemSolver({1}, buildNonlinearBlockSolver(thermal_nonlin_opts, thermal_lin_opts, *mesh_)); + auto coupled_system = combineSystems(coupled_solver, solid_system, thermal_system); + + auto material = makeTimeInfoMaterial( + thermomechanics::ParameterizedGreenSaintVenantThermoelasticMaterial{1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh_->entireBodyName()); + + coupled_system->field_store->getParameterFields()[0].get()->setFromFieldFunction( + [=](smith::tensor) { return 1.0; }); + + solid_system->setDisplacementBC(mesh_->domain("left")); + thermal_system->setTemperatureBC(mesh_->domain("left"), [](auto, auto) { return 1.0; }); + thermal_system->setTemperatureBC(mesh_->domain("right"), [](auto, auto) { return 0.0; }); + + solid_system->addTraction("right", [=](double, auto X, auto, auto, auto, auto, auto... /*params*/) { + auto traction = 0.0 * X; + traction[0] = -0.005; + return traction; + }); + thermal_system->addHeatSource(mesh_->entireBodyName(), [=](auto, auto, auto, auto... /*args*/) { return 0.1; }); + + auto physics = makeDifferentiablePhysics(coupled_system, "staggered_coupled_physics"); + + FunctionalObjective, H1>> qoi( + "staggered_qoi", mesh_, + spaces({coupled_system->field_store->getField("displacement"), + coupled_system->field_store->getField("temperature")})); + qoi.addBodyIntegral(mesh_->entireBodyName(), [](auto, auto, auto U, auto Theta) { + auto u = get(U); + auto theta = get(Theta); + return 0.5 * u[0] * u[0] + 0.05 * theta * theta; + }); + + physics->advanceTimestep(0.5); + auto qoi_fields = std::vector{coupled_system->field_store->getField("displacement"), + coupled_system->field_store->getField("temperature")}; + auto obj = smith::evaluateObjective(qoi, physics->getShapeDispFieldState(), qoi_fields, + TimeInfo(physics->time(), 0.5, static_cast(physics->cycle()))); + + gretl::set_as_objective(obj); + obj.data_store().back_prop(); + + auto param_sens = coupled_system->field_store->getParameterFields()[0].get_dual(); + EXPECT_TRUE(std::isfinite(param_sens->Norml2())); + EXPECT_TRUE(param_sens->Norml2() > 0.0); +} + +// Shared buckling-load magnitudes (used by staggered + monolithic buckling tests). +static constexpr double kBucklingTraction = 0.015; +static constexpr double kBucklingBodyForce = 2.5e-5; +static constexpr double kBucklingHeatSource = 1.0; + +// 3. StaggeredBucklingChallenge +TEST_F(ThermoMechanicsMeshFixture, StaggeredBucklingChallenge) +{ + LinearSolverOptions mech_lin_opts{.linear_solver = LinearSolver::CG, + .preconditioner = Preconditioner::HypreAMG, + .relative_tol = 1e-6, + .absolute_tol = 1e-10, + .max_iterations = 120}; + NonlinearSolverOptions mech_nonlin_opts{ + .nonlin_solver = NonlinearSolver::TrustRegion, .relative_tol = 1e-6, .absolute_tol = 1e-7, .max_iterations = 25}; + LinearSolverOptions therm_lin_opts{.linear_solver = LinearSolver::GMRES, + .preconditioner = Preconditioner::HypreAMG, + .relative_tol = 1e-6, + .absolute_tol = 1e-10, + .max_iterations = 80}; + NonlinearSolverOptions therm_nonlin_opts{.nonlin_solver = NonlinearSolver::NewtonLineSearch, + .relative_tol = 1e-7, + .absolute_tol = 1e-7, + .max_iterations = 12, + .max_line_search_iterations = 6}; + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + + auto solid_system = buildSolidMechanicsSystem(makeSolver(mech_nonlin_opts, mech_lin_opts), SolidMechanicsOptions{}, + solid_fields, couplingFields(thermal_fields)); + auto thermal_system = buildThermalSystem(makeSolver(therm_nonlin_opts, therm_lin_opts), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields)); + + auto coupled_system = combineSystems(solid_system, thermal_system); + auto material = makeTimeInfoMaterial( + thermomechanics::GreenSaintVenantThermoelasticMaterial{1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh_->entireBodyName()); + + applyBucklingLoads(solid_system, thermal_system, kBucklingTraction, kBucklingBodyForce, kBucklingHeatSource); + + double deflection = advanceOneStepAndGetLateralDeflection(coupled_system); + + EXPECT_GT(deflection, 1e-5); +} + +// 4. MonolithicBucklingChallenge +TEST_F(ThermoMechanicsMeshFixture, MonolithicBucklingChallenge) +{ + LinearSolverOptions lin_opts{ + .linear_solver = LinearSolver::SuperLU, .relative_tol = 1e-6, .absolute_tol = 1e-10, .max_iterations = 80}; + NonlinearSolverOptions nonlin_opts{ + .nonlin_solver = NonlinearSolver::Newton, .relative_tol = 1e-7, .absolute_tol = 1e-7, .max_iterations = 12}; + + auto solver_ptr = makeSolver(nonlin_opts, lin_opts); + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + + auto solid_system = + buildSolidMechanicsSystem(nullptr, SolidMechanicsOptions{}, solid_fields, couplingFields(thermal_fields)); + auto thermal_system = buildThermalSystem(nullptr, ThermalOptions{}, thermal_fields, couplingFields(solid_fields)); + + auto coupled_system = combineSystems(solver_ptr, solid_system, thermal_system); + auto material = makeTimeInfoMaterial( + thermomechanics::GreenSaintVenantThermoelasticMaterial{1.0, 100.0, 0.25, 1.0, 0.0025, 0.0, 0.05}); + setCoupledThermoMechanicsMaterial(solid_system, thermal_system, material, mesh_->entireBodyName()); + + applyBucklingLoads(solid_system, thermal_system, kBucklingTraction, kBucklingBodyForce, kBucklingHeatSource); + + double deflection = advanceOneStepAndGetLateralDeflection(coupled_system); + + EXPECT_GT(deflection, 1e-5); +} + +// 5. CauchyStressOutput +TEST_F(ThermoMechanicsMeshFixture, CauchyStressOutput) +{ + SolidMechanicsOptions solid_opts{.enable_stress_output = true, .output_cauchy_stress = true}; + + auto solid_fields = registerSolidMechanicsFields(field_store_, solid_opts); + EXPECT_TRUE(field_store_->hasField(field_store_->prefix("stress"))); + auto solid_system = buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), solid_opts, solid_fields); + ASSERT_EQ(solid_system->post_solve_systems.size(), 1u); + + constexpr double E = 100.0; + constexpr double nu = 0.25; + constexpr double G = E / (2.0 * (1.0 + nu)); + constexpr double K = E / (3.0 * (1.0 - 2.0 * nu)); + + solid_system->setMaterial(makeTimeInfoMaterial(solid_mechanics::NeoHookean{.density = 1.0, .K = K, .G = G}), + mesh_->entireBodyName()); + + solid_system->setDisplacementBC(mesh_->domain("left")); + solid_system->addTraction("right", [](double, auto X, auto, auto, auto, auto, auto... /*params*/) { + auto t = 0.0 * X; + t[0] = -0.01; + return t; + }); + + auto physics = makeDifferentiablePhysics(solid_system, "cauchy_physics"); + physics->advanceTimestep(1.0); + + ASSERT_FALSE(solid_system->post_solve_systems.empty()) << "Stress output system should be present"; + auto states = physics->getFieldStates(); + size_t stress_idx = field_store_->getFieldIndex("stress"); + double stress_norm = norm(*states[stress_idx].get()); + EXPECT_GT(stress_norm, 1e-8) << "Cauchy stress field should be non-zero after deformation"; +} + +TEST_F(ThermoMechanicsMeshFixture, StressOutputRegistrationDisabledByDefault) +{ + auto solid_fields = registerSolidMechanicsFields(field_store_); + EXPECT_FALSE(field_store_->hasField(field_store_->prefix("stress"))); + + auto solid_system = + buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), SolidMechanicsOptions{}, solid_fields); + EXPECT_TRUE(solid_system->post_solve_systems.empty()); +} + +TEST_F(ThermoMechanicsMeshFixture, CombinedSystemCarriesPostSolveSystems) +{ + SolidMechanicsOptions solid_opts{.enable_stress_output = true}; + + auto solid_fields = registerSolidMechanicsFields(field_store_, solid_opts); + auto thermal_fields = registerThermalFields(field_store_); + + auto solid_system = buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), solid_opts, solid_fields, + couplingFields(thermal_fields)); + auto thermal_system = buildThermalSystem(makeSolver(newtonNonlinOpts, directLinOpts), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields)); + + auto combined_system = combineSystems(solid_system, thermal_system); + + ASSERT_EQ(combined_system->post_solve_systems.size(), solid_system->post_solve_systems.size()); + EXPECT_FALSE(combined_system->post_solve_systems.empty()); +} + +TEST_F(ThermoMechanicsMeshFixture, CombinedSystemCarriesCycleZeroSystems) +{ + using DynamicDispRule = ImplicitNewmarkSecondOrderTimeIntegrationRule; + + auto solid_fields = registerSolidMechanicsFields(field_store_); + auto thermal_fields = registerThermalFields(field_store_); + + auto solid_system = buildSolidMechanicsSystem(makeSolver(newtonNonlinOpts, directLinOpts), SolidMechanicsOptions{}, + solid_fields, couplingFields(thermal_fields)); + auto thermal_system = buildThermalSystem(makeSolver(newtonNonlinOpts, directLinOpts), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields)); + + auto combined_system = combineSystems(solid_system, thermal_system); + + ASSERT_EQ(solid_system->cycle_zero_systems.size(), 1u); + EXPECT_EQ(combined_system->cycle_zero_systems.size(), solid_system->cycle_zero_systems.size()); + EXPECT_EQ(combined_system->cycle_zero_systems[0]->solve_result_field_names, std::vector{"acceleration"}); +} + +} // namespace smith + +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + smith::ApplicationManager applicationManager(argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/smith/differentiable_numerics/tests/test_explicit_dynamics.cpp b/src/smith/differentiable_numerics/tests/test_explicit_dynamics.cpp index df4a9c0cb3..18b1c27371 100644 --- a/src/smith/differentiable_numerics/tests/test_explicit_dynamics.cpp +++ b/src/smith/differentiable_numerics/tests/test_explicit_dynamics.cpp @@ -11,7 +11,7 @@ #include "smith/physics/mesh.hpp" #include "gretl/data_store.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" #include "smith/physics/functional_objective.hpp" #include "smith/differentiable_numerics/lumped_mass_explicit_newmark_state_advancer.hpp" @@ -146,7 +146,7 @@ struct MeshFixture : public testing::Test { std::vector trial_spaces = {&disp.get()->space(), &disp.get()->space(), &disp.get()->space(), &density0.get()->space()}; - auto solid_mechanics_residual = std::make_shared>>( physics_name, mesh_, disp.get()->space(), trial_spaces); @@ -162,7 +162,7 @@ struct MeshFixture : public testing::Test { return smith::tuple{smith::get(a) * mat.density(), pk_stress}; }); - solid_mechanics_residual->addBodySource(smith::DependsOn<>{}, mesh_->entireBodyName(), [](auto /*t_info*/, auto X) { + solid_mechanics_residual->addBodySource(mesh_->entireBodyName(), [](auto /*t_info*/, auto X, auto... /*args*/) { auto b = 0.0 * X; b[1] = gravity; return b; @@ -188,18 +188,13 @@ struct MeshFixture : public testing::Test { physics_ = mechanics_; auto ke_objective = std::make_shared>>( - "integrated_squared_temperature", mesh_, smith::spaces({states[DISP], params_[DENSITY]})); + "integrated_kinetic_energy", mesh_, smith::spaces({velo, density0})); - ke_objective->addBodyIntegral(smith::DependsOn<0, 1>(), mesh_->entireBodyName(), - [](auto /*t*/, auto /*X*/, auto U, auto Rho) { - auto u = get(U); - return 0.5 * get(Rho) * smith::inner(u, u); - }); + ke_objective->addBodyIntegral(mesh_->entireBodyName(), [](auto /*t_info*/, auto /*X*/, auto V, auto Rho) { + auto v = get(V); + return 0.5 * get(Rho) * smith::inner(v, v); + }); objective_ = ke_objective; - - // kinetic energy integrator for qoi - kinetic_energy_integrator_ = smith::createKineticEnergyIntegrator( - mesh_->entireBody(), shape_disp_->get()->space(), params_[DENSITY].get()->space()); } void resetAndApplyInitialConditions() @@ -223,8 +218,10 @@ struct MeshFixture : public testing::Test { double base_physics_qoi = 0.0; for (size_t m = 0; m < num_steps; ++m) { physics_->advanceTimestep(dt); - base_physics_qoi += (*kinetic_energy_integrator_)(physics_->time(), physics_->shapeDisplacement(), - physics_->state(velo_name), physics_->parameter(DENSITY)); + std::vector qoi_fields{&physics_->state(velo_name), &physics_->parameter(DENSITY)}; + base_physics_qoi += + objective_->evaluate(smith::TimeInfo(physics_->time(), dt, static_cast(physics_->cycle())), + &physics_->shapeDisplacement(), qoi_fields); } return base_physics_qoi; @@ -237,20 +234,15 @@ struct MeshFixture : public testing::Test { physics_->state(velo_name).name() + "_adjoint_load"); physics_->resetAdjointStates(); while (physics_->cycle() > 0) { - auto shape_sensitivity_op = smith::get( - (*kinetic_energy_integrator_)(physics_->time(), differentiate_wrt(physics_->shapeDisplacement()), - physics_->state(velo_name), physics_->parameter(DENSITY))); - shape_sensitivity += *assemble(shape_sensitivity_op); + auto qoi_time_info = smith::TimeInfo(physics_->time(), dt, static_cast(physics_->cycle())); + std::vector qoi_fields{&physics_->state(velo_name), &physics_->parameter(DENSITY)}; - auto density_sensitivity_op = smith::get( - (*kinetic_energy_integrator_)(physics_->time(), physics_->shapeDisplacement(), physics_->state(velo_name), - differentiate_wrt(physics_->parameter(DENSITY)))); - parameter_sensitivities[DENSITY] += *assemble(density_sensitivity_op); + shape_sensitivity += + objective_->mesh_coordinate_gradient(qoi_time_info, &physics_->shapeDisplacement(), qoi_fields); + parameter_sensitivities[DENSITY] += + objective_->gradient(qoi_time_info, &physics_->shapeDisplacement(), qoi_fields, 1); - auto velo_sensivitity_op = smith::get((*kinetic_energy_integrator_)( - physics_->time(), physics_->shapeDisplacement(), smith::differentiate_wrt(physics_->state(velo_name)), - physics_->parameter(DENSITY))); - velo_adjoint_load = *assemble(velo_sensivitity_op); + velo_adjoint_load = objective_->gradient(qoi_time_info, &physics_->shapeDisplacement(), qoi_fields, 0); physics_->setAdjointLoad({{velo_name, velo_adjoint_load}}); physics_->reverseAdjointTimestep(); @@ -275,7 +267,6 @@ struct MeshFixture : public testing::Test { std::shared_ptr physics_; std::shared_ptr objective_; - std::shared_ptr> kinetic_energy_integrator_; std::shared_ptr bc_manager_; @@ -328,7 +319,9 @@ TEST_F(MeshFixture, TransientDynamicsGretl) auto all_fields = mechanics_->getFieldStatesAndParamStates(); gretl::State gretl_qoi = - 0.0 * smith::evaluateObjective(*objective_, *shape_disp_, {all_fields[F_VELO], all_fields[F_DENSITY]}); + 0.0 * + smith::evaluateObjective(*objective_, *shape_disp_, {all_fields[F_VELO], all_fields[F_DENSITY]}, + smith::TimeInfo(mechanics_->time(), 1.0, static_cast(mechanics_->cycle()))); std::string pv_dir = std::string("paraview_") + mechanics_->name(); auto pv_writer = smith::createParaviewWriter(*mesh_, all_fields, pv_dir); @@ -336,8 +329,9 @@ TEST_F(MeshFixture, TransientDynamicsGretl) for (size_t m = 0; m < num_steps; ++m) { mechanics_->advanceTimestep(dt); all_fields = mechanics_->getFieldStatesAndParamStates(); - gretl_qoi = - gretl_qoi + smith::evaluateObjective(*objective_, *shape_disp_, {all_fields[F_VELO], all_fields[F_DENSITY]}); + gretl_qoi = gretl_qoi + smith::evaluateObjective( + *objective_, *shape_disp_, {all_fields[F_VELO], all_fields[F_DENSITY]}, + smith::TimeInfo(mechanics_->time(), dt, static_cast(mechanics_->cycle()))); pv_writer.write(mechanics_->cycle(), mechanics_->time(), all_fields); } @@ -390,20 +384,19 @@ TEST_F(MeshFixture, TransientConstantGravity) smith::FunctionalObjective> accel_error("accel_error", mesh_, smith::spaces({all_fields[ACCEL]})); - accel_error.addBodyIntegral(smith::DependsOn<0>{}, mesh_->entireBodyName(), - [a_exact](auto /*t*/, auto /*X*/, auto A) { - auto a = smith::get(A); - auto da0 = a[0]; - auto da1 = a[1] - a_exact; - return da0 * da0 + da1 * da1; - }); + accel_error.addBodyIntegral(mesh_->entireBodyName(), [a_exact](auto /*t*/, auto /*X*/, auto A) { + auto a = smith::get(A); + auto da0 = a[0]; + auto da1 = a[1] - a_exact; + return da0 * da0 + da1 * da1; + }); double a_err = accel_error.evaluate(smith::TimeInfo(0.0, 1.0, 0), shape_disp_->get().get(), smith::getConstFieldPointers({all_fields[ACCEL]})); EXPECT_NEAR(0.0, a_err, 1e-14); smith::FunctionalObjective> velo_error("velo_error", mesh_, smith::spaces({all_fields[VELO]})); - velo_error.addBodyIntegral(smith::DependsOn<0>{}, mesh_->entireBodyName(), [v_exact](auto /*t*/, auto /*X*/, auto V) { + velo_error.addBodyIntegral(mesh_->entireBodyName(), [v_exact](auto /*t*/, auto /*X*/, auto V) { auto v = smith::get(V); auto dv0 = v[0]; auto dv1 = v[1] - v_exact; @@ -415,7 +408,7 @@ TEST_F(MeshFixture, TransientConstantGravity) smith::FunctionalObjective> disp_error("disp_error", mesh_, smith::spaces({all_fields[DISP]})); - disp_error.addBodyIntegral(smith::DependsOn<0>{}, mesh_->entireBodyName(), [u_exact](auto /*t*/, auto /*X*/, auto U) { + disp_error.addBodyIntegral(mesh_->entireBodyName(), [u_exact](auto /*t*/, auto /*X*/, auto U) { auto u = smith::get(U); auto du0 = u[0]; auto du1 = u[1] - u_exact; diff --git a/src/smith/differentiable_numerics/tests/test_make_time_info_material.cpp b/src/smith/differentiable_numerics/tests/test_make_time_info_material.cpp new file mode 100644 index 0000000000..3cbdb0fee4 --- /dev/null +++ b/src/smith/differentiable_numerics/tests/test_make_time_info_material.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "gtest/gtest.h" + +#include "smith/differentiable_numerics/make_time_info_material.hpp" +#include "smith/physics/common.hpp" + +namespace smith { + +struct TestMaterialState {}; + +struct StaticMaterial { + using State = TestMaterialState; + + double density = 2.0; + + double operator()(State&, double grad_u, double param) const { return density + grad_u + param; } +}; + +TEST(TimeInfoMaterial, ForwardsStaticMaterial) +{ + auto material = makeTimeInfoMaterial(StaticMaterial{}); + StaticMaterial::State state; + + EXPECT_EQ(material.density, 2.0); + EXPECT_EQ(material(TimeInfo(10.0, 20.0), state, 1.0, 100.0, 3.0), 6.0); + EXPECT_EQ(material.material.density, 2.0); +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/tests/test_multiphysics_time_integrator.cpp b/src/smith/differentiable_numerics/tests/test_multiphysics_time_integrator.cpp index 86dafb41d2..c2cb034247 100644 --- a/src/smith/differentiable_numerics/tests/test_multiphysics_time_integrator.cpp +++ b/src/smith/differentiable_numerics/tests/test_multiphysics_time_integrator.cpp @@ -16,12 +16,12 @@ #include "smith/physics/mesh.hpp" #include "smith/physics/state/state_manager.hpp" -#include "smith/differentiable_numerics/coupled_system_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" #include "smith/differentiable_numerics/field_store.hpp" #include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" namespace smith { @@ -76,6 +76,51 @@ class CountingNoOpNonlinearBlockSolver : public NoOpNonlinearBlockSolver { } }; +class ConstantNonlinearBlockSolver : public NonlinearBlockSolverBase { + public: + explicit ConstantNonlinearBlockSolver(double value) : value_(value) {} + + std::vector solve( + const std::vector& u_guesses, std::function(const std::vector&)>, + std::function>(const std::vector&)>) const override + { + ++solve_calls_; + std::vector solved; + solved.reserve(u_guesses.size()); + for (const auto& guess : u_guesses) { + auto state = std::make_shared(*guess); + *state = value_; + solved.push_back(state); + } + return solved; + } + + std::vector solveAdjoint(const std::vector&, std::vector>&) const override + { + return {}; + } + + ConvergenceStatus convergenceStatus(double, const std::vector& residuals, + NonlinearConvergenceContext&) const override + { + ConvergenceStatus status; + status.global_converged = true; + status.converged = true; + status.block_norms.resize(residuals.size(), 0.0); + return status; + } + + void primeConvergenceContext(const std::vector&, NonlinearConvergenceContext&) const override {} + + int solveCalls() const { return solve_calls_; } + + void setInnerToleranceMultiplier(double) override {} + + private: + double value_; + mutable int solve_calls_ = 0; +}; + class NeedsInitialSolveRule : public QuasiStaticRule { public: bool requiresInitialAccelerationSolve() const override { return true; } @@ -85,10 +130,10 @@ template auto buildScalarDiffusionWeakForm(const std::string& name, std::shared_ptr mesh, std::shared_ptr fs, FieldTypeT field_type) { - using WeakFormType = TimeDiscretizedWeakForm<2, H1<1>, Parameters>>; + using WeakFormType = FunctionalWeakForm<2, H1<1>, Parameters>>; auto weak_form = std::make_shared(name, mesh, fs->getField(field_type.name).get()->space(), fs->createSpaces(name, field_type.name, field_type)); - weak_form->addBodyIntegral(DependsOn<0>{}, mesh->entireBodyName(), + weak_form->addBodyIntegral(mesh->entireBodyName(), [](auto, auto, auto u) { return smith::tuple{0.0 * get(u), get(u)}; }); return weak_form; } @@ -98,7 +143,7 @@ auto buildSecondOrderMainWeakForm(const std::string& name, std::shared_ptr DispFieldType displacement_type, DispOldFieldType displacement_old_type, VelocityFieldType velocity_type, AccelerationFieldType acceleration_type) { - using WeakFormType = TimeDiscretizedWeakForm<2, H1<1>, Parameters, H1<1>, H1<1>, H1<1>>>; + using WeakFormType = FunctionalWeakForm<2, H1<1>, Parameters, H1<1>, H1<1>, H1<1>>>; auto weak_form = std::make_shared(name, mesh, fs->getField(displacement_type.name).get()->space(), fs->createSpaces(name, displacement_type.name, displacement_type, @@ -112,7 +157,7 @@ auto buildSecondOrderCycleZeroWeakForm(const std::string& name, std::shared_ptr< std::shared_ptr fs, DispFieldType displacement_type, VelocityFieldType velocity_type, AccelerationFieldType acceleration_type) { - using WeakFormType = TimeDiscretizedWeakForm<2, H1<1>, Parameters, H1<1>, H1<1>>>; + using WeakFormType = FunctionalWeakForm<2, H1<1>, Parameters, H1<1>, H1<1>>>; auto weak_form = std::make_shared( name, mesh, fs->getField(acceleration_type.name).get()->space(), fs->createSpaces(name, acceleration_type.name, displacement_type, velocity_type, acceleration_type)); @@ -141,7 +186,8 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroUsesBcsForReactionFieldNotUnknownZero) [](std::vector nodes, int) { return allNodesOnBoundary(nodes, 1.0); }); auto field_store = std::make_shared(mesh, 20); - field_store->addShapeDisp(FieldType>("shape_displacement")); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); auto quasi_static = std::make_shared(); FieldType> temperature_type("temperature"); @@ -149,8 +195,8 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroUsesBcsForReactionFieldNotUnknownZero) FieldType> displacement_type("displacement"); auto displacement_bc = field_store->addIndependent(displacement_type, quasi_static); - ASSERT_EQ(field_store->getUnknownIndex("temperature"), 0); - ASSERT_EQ(field_store->getUnknownIndex("displacement"), 1); + ASSERT_TRUE(temperature_type.is_unknown); + ASSERT_TRUE(displacement_type.is_unknown); temperature_bc->setScalarBCs<2>(mesh->domain("left"), [](double, tensor) { return 0.0; }); displacement_bc->setScalarBCs<2>(mesh->domain("right"), [](double, tensor) { return 1.0; }); @@ -159,16 +205,25 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroUsesBcsForReactionFieldNotUnknownZero) auto displacement_wf = buildScalarDiffusionWeakForm("displacement_main", mesh, field_store, displacement_type); auto cycle_zero_wf = buildScalarDiffusionWeakForm("cycle_zero_displacement", mesh, field_store, displacement_type); - auto main_solver = std::make_shared(std::make_shared()); + auto main_solver = std::make_shared(std::make_shared()); LinearSolverOptions lin_opts{.linear_solver = LinearSolver::SuperLU}; NonlinearSolverOptions nonlin_opts{ .nonlin_solver = NonlinearSolver::Newton, .relative_tol = 1.0e-12, .absolute_tol = 1.0e-12, .max_iterations = 8}; auto cycle_zero_block_solver = buildNonlinearBlockSolver(nonlin_opts, lin_opts, *mesh); - auto cycle_zero_solver = std::make_shared(cycle_zero_block_solver); + auto cycle_zero_solver = std::make_shared(cycle_zero_block_solver); - MultiphysicsTimeIntegrator advancer(field_store, {temperature_wf, displacement_wf}, main_solver, cycle_zero_wf, - cycle_zero_solver); + auto main_system = std::make_shared(); + main_system->field_store = field_store; + main_system->weak_forms = {temperature_wf, displacement_wf}; + main_system->solver = main_solver; + + auto cycle_zero_system = std::make_shared(); + cycle_zero_system->field_store = field_store; + cycle_zero_system->weak_forms = {cycle_zero_wf}; + cycle_zero_system->solver = cycle_zero_solver; + + MultiphysicsTimeIntegrator advancer(main_system, {cycle_zero_system}); auto [new_states, reactions] = advancer.advanceState(TimeInfo(0.0, 1.0, 0), field_store->getShapeDisp(), field_store->getAllFields(), {}); @@ -193,7 +248,8 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroSkippedForQuasiStaticSecondOrderRule) "integrator_mesh"); auto field_store = std::make_shared(mesh, 20); - field_store->addShapeDisp(FieldType>("shape_displacement")); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); auto quasi_static = std::make_shared(); FieldType> displacement_type("displacement_solve_state"); @@ -209,11 +265,21 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroSkippedForQuasiStaticSecondOrderRule) auto cycle_zero_wf = buildSecondOrderCycleZeroWeakForm("cycle_zero_acceleration", mesh, field_store, displacement_type, velocity_type, acceleration_type); - auto main_solver = std::make_shared(std::make_shared()); + auto main_solver = std::make_shared(std::make_shared()); auto cycle_zero_block_solver = std::make_shared(); - auto cycle_zero_solver = std::make_shared(cycle_zero_block_solver); + auto cycle_zero_solver = std::make_shared(cycle_zero_block_solver); + + auto main_system = std::make_shared(); + main_system->field_store = field_store; + main_system->weak_forms = {main_wf}; + main_system->solver = main_solver; - MultiphysicsTimeIntegrator advancer(field_store, {main_wf}, main_solver, cycle_zero_wf, cycle_zero_solver); + auto cycle_zero_system = std::make_shared(); + cycle_zero_system->field_store = field_store; + cycle_zero_system->weak_forms = {cycle_zero_wf}; + cycle_zero_system->solver = cycle_zero_solver; + + MultiphysicsTimeIntegrator advancer(main_system, {cycle_zero_system}); auto [new_states, reactions] = advancer.advanceState(TimeInfo(0.0, 1.0, 0), field_store->getShapeDisp(), field_store->getAllFields(), {}); @@ -225,7 +291,128 @@ TEST(MultiphysicsTimeIntegrator, CycleZeroSkippedForQuasiStaticSecondOrderRule) StateManager::reset(); } -TEST(CoupledSystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequestedBlock) +TEST(MultiphysicsTimeIntegrator, CycleZeroSolveResultUpdatesAccelerationField) +{ + axom::sidre::DataStore datastore; + StateManager::initialize(datastore, "cycle_zero_solve_result_updates_acceleration"); + + auto mesh = std::make_shared(mfem::Mesh::MakeCartesian2D(4, 4, mfem::Element::QUADRILATERAL, true, 1.0, 1.0), + "cycle_zero_update_mesh"); + + auto field_store = std::make_shared(mesh, 20); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); + + auto time_rule = std::make_shared(); + auto static_rule = std::make_shared(); + FieldType> displacement_type("displacement_solve_state"); + field_store->addIndependent(displacement_type, time_rule); + FieldType> displacement_old_type("displacement"); + FieldType> velocity_type("velocity"); + FieldType> acceleration_type("acceleration"); + field_store->addIndependent(displacement_old_type, static_rule); + field_store->addIndependent(velocity_type, static_rule); + field_store->addIndependent(acceleration_type, static_rule); + + *field_store->getField(displacement_type.name).get() = 3.0; + *field_store->getField(acceleration_type.name).get() = -2.0; + + auto main_wf = buildScalarDiffusionWeakForm("displacement_main", mesh, field_store, displacement_type); + auto cycle_zero_wf = buildSecondOrderMainWeakForm("cycle_zero_acceleration", mesh, field_store, displacement_type, + displacement_old_type, velocity_type, acceleration_type); + + auto main_block_solver = std::make_shared(); + auto main_solver = std::make_shared(main_block_solver); + auto cycle_zero_block_solver = std::make_shared(9.0); + auto cycle_zero_solver = std::make_shared(cycle_zero_block_solver); + + auto main_system = std::make_shared(); + main_system->field_store = field_store; + main_system->weak_forms = {main_wf}; + main_system->solver = main_solver; + + auto cycle_zero_system = std::make_shared(); + cycle_zero_system->field_store = field_store; + cycle_zero_system->weak_forms = {cycle_zero_wf}; + cycle_zero_system->solver = cycle_zero_solver; + cycle_zero_system->solve_result_field_names = {acceleration_type.name}; + cycle_zero_system->solve_input_field_names = { + {displacement_old_type.name, displacement_old_type.name, velocity_type.name, acceleration_type.name}}; + + MultiphysicsTimeIntegrator advancer(main_system, {cycle_zero_system}); + + auto [new_states, reactions] = + advancer.advanceState(TimeInfo(0.0, 1.0, 0), field_store->getShapeDisp(), field_store->getAllFields(), {}); + static_cast(reactions); + + EXPECT_EQ(cycle_zero_block_solver->solveCalls(), 1); + EXPECT_EQ(main_block_solver->solveCalls(), 1); + EXPECT_NEAR((*new_states[field_store->getFieldIndex(acceleration_type.name)].get())(0), 9.0, 1.0e-12); + EXPECT_NEAR((*new_states[field_store->getFieldIndex(displacement_type.name)].get())(0), 3.0, 1.0e-12); + + StateManager::reset(); +} + +TEST(MultiphysicsTimeIntegrator, CycleZeroAccelerationBcUsesDisplacementSecondTimeDerivative) +{ + axom::sidre::DataStore datastore; + StateManager::initialize(datastore, "cycle_zero_acceleration_bc_uses_displacement_second_time_derivative"); + + auto mesh = std::make_shared(mfem::Mesh::MakeCartesian2D(4, 4, mfem::Element::QUADRILATERAL, true, 1.0, 1.0), + "cycle_zero_bc_mesh"); + mesh->addDomainOfBoundaryElements("left", + [](std::vector nodes, int) { return allNodesOnBoundary(nodes, 0.0); }); + mesh->addDomainOfBoundaryElements("right", + [](std::vector nodes, int) { return allNodesOnBoundary(nodes, 1.0); }); + + auto field_store = std::make_shared(mesh, 20); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); + + auto time_rule = std::make_shared(); + FieldType> displacement_type("displacement_solve_state"); + auto displacement_bc = field_store->addIndependent(displacement_type, time_rule); + auto displacement_old_type = + field_store->addDependent(displacement_type, FieldStore::TimeDerivative::VAL, "displacement"); + auto velocity_type = field_store->addDependent(displacement_type, FieldStore::TimeDerivative::DOT, "velocity"); + auto acceleration_type = + field_store->addDependent(displacement_type, FieldStore::TimeDerivative::DDOT, "acceleration"); + + auto cycle_zero_wf = buildSecondOrderCycleZeroWeakForm("cycle_zero_acceleration", mesh, field_store, + displacement_old_type, velocity_type, acceleration_type); + + displacement_bc->setScalarBCs<2>(mesh->domain("right"), [](double t, tensor) { return t * t; }); + + auto bc_managers = field_store->getBoundaryConditionManagers({cycle_zero_wf->name()}); + auto acceleration_bc_managers = field_store->getBoundaryConditionManagersForFields({acceleration_type.name}); + ASSERT_EQ(acceleration_bc_managers.size(), 1); + ASSERT_NE(acceleration_bc_managers[0], nullptr); + ASSERT_EQ(bc_managers.size(), 1); + ASSERT_NE(bc_managers[0], nullptr); + EXPECT_EQ(acceleration_bc_managers[0]->allEssentialTrueDofs().Size(), bc_managers[0]->allEssentialTrueDofs().Size()); + const int right_dofs = bc_managers[0]->allEssentialTrueDofs().Size(); + ASSERT_GT(right_dofs, 0); + + displacement_bc->setScalarBCs<2>(mesh->domain("left"), [](double t, tensor) { return t * t; }); + + bc_managers = field_store->getBoundaryConditionManagers({cycle_zero_wf->name()}); + ASSERT_NE(bc_managers[0], nullptr); + EXPECT_GT(bc_managers[0]->allEssentialTrueDofs().Size(), right_dofs); + + mfem::Vector acceleration(field_store->getField("acceleration").get()->space().GetTrueVSize()); + acceleration = 0.0; + for (const auto& bc : bc_managers[0]->essentials()) { + bc.setDofs(acceleration, 0.0); + } + + for (int dof : bc_managers[0]->allEssentialTrueDofs()) { + EXPECT_NEAR(acceleration[dof], 2.0, 1.0e-7); + } + + StateManager::reset(); +} + +TEST(SystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequestedBlock) { axom::sidre::DataStore datastore; StateManager::initialize(datastore, "coupled_system_solver_single_block_characterization"); @@ -234,7 +421,8 @@ TEST(CoupledSystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequested "single_block_characterization_mesh"); auto field_store = std::make_shared(mesh, 20); - field_store->addShapeDisp(FieldType>("shape_displacement")); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); auto quasi_static = std::make_shared(); FieldType> temperature_type("temperature"); @@ -246,7 +434,7 @@ TEST(CoupledSystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequested auto displacement_wf = buildScalarDiffusionWeakForm("displacement_main", mesh, field_store, displacement_type); auto recording_solver = std::make_shared(); - auto monolithic_solver = std::make_shared(recording_solver); + auto monolithic_solver = std::make_shared(recording_solver); auto derived_single_block_solver = monolithic_solver->singleBlockSolver(0); ASSERT_NE(derived_single_block_solver, nullptr); @@ -257,7 +445,7 @@ TEST(CoupledSystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequested const std::vector> states = {field_store->getStates("temperature_main"), field_store->getStates("displacement_main")}; const std::vector> params(residuals.size()); - const auto bc_managers = field_store->getBoundaryConditionManagers(); + const auto bc_managers = field_store->getBoundaryConditionManagers(residual_names); auto solved_states = derived_single_block_solver->solve(residuals, block_indices, field_store->getShapeDisp(), states, params, TimeInfo(0.0, 1.0, 0), bc_managers); @@ -269,6 +457,63 @@ TEST(CoupledSystemSolver, SingleBlockSolverFromMonolithicStageNarrowsToRequested StateManager::reset(); } +TEST(SystemSolver, AppendsStagesWithBlockMappingForCombinedSubsystems) +{ + auto first_solver = std::make_shared(); + auto second_solver = std::make_shared(); + + SystemSolver subsystem_a(3, false); + subsystem_a.addSubsystemSolver({0}, first_solver, 0.5); + + SystemSolver subsystem_b(3, false); + subsystem_b.addSubsystemSolver({0, 1}, second_solver, 1.0); + + SystemSolver combined_solver(3, false); + combined_solver.appendStagesWithBlockMapping(subsystem_a, {0}); + combined_solver.appendStagesWithBlockMapping(subsystem_b, {1, 2}); + + axom::sidre::DataStore datastore; + StateManager::initialize(datastore, "combined_solver_stage_mapping"); + + auto mesh = std::make_shared(mfem::Mesh::MakeCartesian2D(2, 2, mfem::Element::QUADRILATERAL, true, 1.0, 1.0), + "combined_solver_stage_mapping_mesh"); + + auto field_store = std::make_shared(mesh, 20); + FieldType> shape_disp_type("shape_displacement"); + field_store->addShapeDisp(shape_disp_type); + + auto static_rule = std::make_shared(); + FieldType> field0_type("field0"); + FieldType> field1_type("field1"); + FieldType> field2_type("field2"); + field_store->addIndependent(field0_type, static_rule); + field_store->addIndependent(field1_type, static_rule); + field_store->addIndependent(field2_type, static_rule); + + auto wf0 = buildScalarDiffusionWeakForm("wf0", mesh, field_store, field0_type); + auto wf1 = buildScalarDiffusionWeakForm("wf1", mesh, field_store, field1_type); + auto wf2 = buildScalarDiffusionWeakForm("wf2", mesh, field_store, field2_type); + + const std::vector residuals = {wf0.get(), wf1.get(), wf2.get()}; + const std::vector residual_names = {"wf0", "wf1", "wf2"}; + const auto block_indices = field_store->indexMap(residual_names); + const std::vector> states = {field_store->getStates("wf0"), field_store->getStates("wf1"), + field_store->getStates("wf2")}; + const std::vector> params(residuals.size()); + const auto bc_managers = field_store->getBoundaryConditionManagers(residual_names); + + auto solved_states = combined_solver.solve(residuals, block_indices, field_store->getShapeDisp(), states, params, + TimeInfo(0.0, 1.0, 0), bc_managers); + + EXPECT_EQ(solved_states.size(), 3); + EXPECT_EQ(first_solver->solveCalls(), 1); + EXPECT_EQ(first_solver->lastNumUnknowns(), 1); + EXPECT_EQ(second_solver->solveCalls(), 1); + EXPECT_EQ(second_solver->lastNumUnknowns(), 2); + + StateManager::reset(); +} + } // namespace smith int main(int argc, char* argv[]) diff --git a/src/smith/differentiable_numerics/tests/test_porous_heat_sink.cpp b/src/smith/differentiable_numerics/tests/test_porous_heat_sink.cpp index 3944016934..e7d218dd1f 100644 --- a/src/smith/differentiable_numerics/tests/test_porous_heat_sink.cpp +++ b/src/smith/differentiable_numerics/tests/test_porous_heat_sink.cpp @@ -184,21 +184,20 @@ TEST_P(BlockPreconditionerTest, BlockSolve) auto gamma_coef = std::make_shared(gamma_fun); gamma.get()->project(gamma_coef); - T1_form.addBodyIntegral(DependsOn<0, 1, 2>{}, mesh->entireBodyName(), - [sigma, a, q_n](double /* t */, auto /* x */, auto T_1, auto T_2, auto gamma_) { + T1_form.addBodyIntegral(mesh->entireBodyName(), + [sigma, a, q_n](auto /* t_info */, auto /* x */, auto T_1, auto T_2, auto gamma_) { auto [T_1_val, dT_1_dX] = T_1; auto [T_2_val, dT_2_dX] = T_2; auto [gamma_val, dgamma_dX] = gamma_; return smith::tuple{a(gamma_val) * q_n(T_1_val, T_2_val), sigma(gamma_val) * dT_1_dX}; }); - T2_form.addBodyIntegral(DependsOn<0, 1, 2>{}, mesh->entireBodyName(), - [kappa, a, q_n](double /* t */, auto /* x */, auto T_1, auto T_2, auto gamma_) { - auto [T_1_val, dT_1_dX] = T_1; - auto [T_2_val, dT_2_dX] = T_2; - auto [gamma_val, dgamma_dX] = gamma_; - return smith::tuple{-1.0 * a(gamma_val) * q_n(T_1_val, T_2_val), - kappa(gamma_val) * dT_2_dX}; - }); + T2_form.addBodyIntegral( + mesh->entireBodyName(), [kappa, a, q_n](auto /* t_info */, auto /* x */, auto T_1, auto T_2, auto gamma_) { + auto [T_1_val, dT_1_dX] = T_1; + auto [T_2_val, dT_2_dX] = T_2; + auto [gamma_val, dgamma_dX] = gamma_; + return smith::tuple{-1.0 * a(gamma_val) * q_n(T_1_val, T_2_val), kappa(gamma_val) * dT_2_dX}; + }); auto T1_bc_manager = std::make_shared(mesh->mfemParMesh()); auto T2_bc_manager = std::make_shared(mesh->mfemParMesh()); @@ -207,8 +206,9 @@ TEST_P(BlockPreconditionerTest, BlockSolve) T2_bc_manager->addEssential(std::set{1}, zero_bcs, space(T2), 0); mesh->addDomainOfBoundaryElements("heat_spreader", by_attr<2>(2)); - T1_form.addBoundaryIntegral(DependsOn<>{}, "heat_spreader", - [heatsink_options = heatsink_options](double, auto) { return heatsink_options.q_app; }); + T1_form.addBoundaryIntegral("heat_spreader", [heatsink_options = heatsink_options](auto, auto, auto... /*args*/) { + return heatsink_options.q_app; + }); // Block Preconditioner Options smith::LinearSolverOptions linear_options; diff --git a/src/smith/differentiable_numerics/tests/test_solid_dynamics.cpp b/src/smith/differentiable_numerics/tests/test_solid_dynamics.cpp index df241483df..333e8d39c0 100644 --- a/src/smith/differentiable_numerics/tests/test_solid_dynamics.cpp +++ b/src/smith/differentiable_numerics/tests/test_solid_dynamics.cpp @@ -7,24 +7,25 @@ #include "gtest/gtest.h" #include "gretl/data_store.hpp" +#include +#include -#include "smith/smith_config.hpp" #include "smith/infrastructure/application_manager.hpp" -#include "smith/numerics/equation_solver.hpp" #include "smith/numerics/solver_config.hpp" -#include "smith/mesh_utils/mesh_utils.hpp" #include "smith/physics/state/state_manager.hpp" #include "smith/physics/functional_objective.hpp" #include "smith/physics/boundary_conditions/boundary_condition_manager.hpp" #include "smith/physics/materials/parameterized_solid_material.hpp" +#include "smith/physics/materials/solid_material.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" -#include "smith/differentiable_numerics/coupled_system_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" #include "smith/differentiable_numerics/paraview_writer.hpp" #include "smith/differentiable_numerics/differentiable_test_utils.hpp" #include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/make_time_info_material.hpp" namespace smith { @@ -46,19 +47,6 @@ inline void checkUnconstrainedReactions(const FiniteElementDual& reaction, const << "Reaction forces should be zero at non-Dirichlet DOFs. Max violation: " << max_unconstrained; } -LinearSolverOptions solid_linear_options{.linear_solver = LinearSolver::CG, - .preconditioner = Preconditioner::HypreJacobi, - .relative_tol = 1e-11, - .absolute_tol = 1e-11, - .max_iterations = 10000, - .print_level = 0}; - -NonlinearSolverOptions solid_nonlinear_opts{.nonlin_solver = NonlinearSolver::TrustRegion, - .relative_tol = 1.0e-10, - .absolute_tol = 1.0e-10, - .max_iterations = 500, - .print_level = 1}; - static constexpr int dim = 3; static constexpr int order = 1; @@ -67,6 +55,19 @@ using VectorSpace = H1; using ScalarParameterSpace = L2<0>; struct SolidMechanicsMeshFixture : public testing::Test { + LinearSolverOptions solid_linear_options{.linear_solver = LinearSolver::CG, + .preconditioner = Preconditioner::HypreJacobi, + .relative_tol = 1e-11, + .absolute_tol = 1e-11, + .max_iterations = 10000, + .print_level = 0}; + + NonlinearSolverOptions solid_nonlinear_opts{.nonlin_solver = NonlinearSolver::TrustRegion, + .relative_tol = 1.0e-10, + .absolute_tol = 1.0e-10, + .max_iterations = 500, + .print_level = 0}; + double length = 1.0; double width = 0.04; int num_elements_x = 12; @@ -93,16 +94,94 @@ struct SolidMechanicsMeshFixture : public testing::Test { std::shared_ptr mesh; }; +// Verifies the cycle-zero contract: rules that report requiresInitialAccelerationSolve() +// produce a non-empty cycle_zero_systems; rules that don't (QuasiStatic) produce empty. +TEST_F(SolidMechanicsMeshFixture, CycleZeroSystemPresenceMatchesRuleContract) +{ + auto makeSolver = [&] { + return std::make_shared(buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh)); + }; + { + auto field_store = fieldStore(mesh, 200, "impl"); + auto solid_fields = + registerSolidMechanicsFields(field_store); + auto solid_system = buildSolidMechanicsSystem(makeSolver(), SolidMechanicsOptions{}, solid_fields); + EXPECT_EQ(solid_system->cycle_zero_systems.size(), 1u) + << "ImplicitNewmark should emit a cycle-zero initial acceleration solve"; + } + { + auto field_store = fieldStore(mesh, 200, "qs"); + auto solid_fields = + registerSolidMechanicsFields(field_store); + auto solid_system = buildSolidMechanicsSystem(makeSolver(), SolidMechanicsOptions{}, solid_fields); + EXPECT_TRUE(solid_system->cycle_zero_systems.empty()) << "QuasiStatic has no initial acceleration solve"; + } +} + +TEST_F(SolidMechanicsMeshFixture, CycleZeroSolverUsesOwnedSingleStepPolicy) +{ + auto main_stage_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto solver = std::make_shared(7, false); + solver->addSubsystemSolver({0}, main_stage_solver, 0.8); + + auto field_store = std::make_shared(mesh, 100, "cycle_zero_policy"); + using TimeRule = ImplicitNewmarkSecondOrderTimeIntegrationRule; + auto solid_fields = registerSolidMechanicsFields(field_store); + auto solid_system = buildSolidMechanicsSystem(solver, SolidMechanicsOptions{}, solid_fields); + + ASSERT_EQ(solid_system->cycle_zero_systems.size(), 1u); + const auto& cz = solid_system->cycle_zero_systems[0]; + ASSERT_NE(cz->solver, nullptr); + EXPECT_EQ(cz->solver->maxStaggeredIterations(), 1); + EXPECT_TRUE(cz->solver->exactStaggeredSteps()); +} + +TEST_F(SolidMechanicsMeshFixture, CycleZeroSolverFallbackBuildsWithoutMainSolver) +{ + auto field_store = std::make_shared(mesh, 100, "cycle_zero_fallback"); + using TimeRule = ImplicitNewmarkSecondOrderTimeIntegrationRule; + auto solid_fields = registerSolidMechanicsFields(field_store); + auto solid_system = buildSolidMechanicsSystem(nullptr, SolidMechanicsOptions{}, solid_fields); + + ASSERT_EQ(solid_system->cycle_zero_systems.size(), 1u); + const auto& cz = solid_system->cycle_zero_systems[0]; + ASSERT_NE(cz->solver, nullptr); + EXPECT_EQ(cz->solver->maxStaggeredIterations(), 1); + EXPECT_FALSE(cz->solver->exactStaggeredSteps()); +} + +TEST(ImplicitNewmarkSecondOrderRule, CycleZeroModeUsesFourthArgumentAsCurrentAcceleration) +{ + ImplicitNewmarkSecondOrderTimeIntegrationRule rule; + TimeInfo regular_time(0.0, 2.0, 0); + TimeInfo cycle_zero_time(0.0, 2.0, 0, TimeInfo::EvaluationMode::CycleZero); + + const double u_new = 10.0; + const double u_old = 4.0; + const double v_old = 3.0; + const double a_arg = 7.0; + + EXPECT_DOUBLE_EQ(rule.value(regular_time, u_new, u_old, v_old, a_arg), u_new); + EXPECT_DOUBLE_EQ(rule.dot(regular_time, u_new, u_old, v_old, a_arg), 3.0); + EXPECT_DOUBLE_EQ(rule.ddot(regular_time, u_new, u_old, v_old, a_arg), -7.0); + + EXPECT_DOUBLE_EQ(rule.value(cycle_zero_time, u_new, u_old, v_old, a_arg), u_old); + EXPECT_DOUBLE_EQ(rule.dot(cycle_zero_time, u_new, u_old, v_old, a_arg), v_old); + EXPECT_DOUBLE_EQ(rule.ddot(cycle_zero_time, u_new, u_old, v_old, a_arg), a_arg); + + EXPECT_DOUBLE_EQ(rule.value(regular_time, u_old, u_old, v_old, a_arg), u_old); + EXPECT_DOUBLE_EQ(rule.dot(regular_time, u_old, u_old, v_old, a_arg), -v_old); + EXPECT_DOUBLE_EQ(rule.ddot(regular_time, u_old, u_old, v_old, a_arg), -6.0 - a_arg); +} + TEST_F(SolidMechanicsMeshFixture, TransientConstantGravity) { SMITH_MARK_FUNCTION; - auto solid_block_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); - - auto coupled_solver = std::make_shared(solid_block_solver); - auto system = buildSolidMechanicsSystem( - mesh, coupled_solver, ImplicitNewmarkSecondOrderTimeIntegrationRule{}, FieldType("bulk"), - FieldType("shear")); + SolidMechanicsOptions solid_opts{.enable_stress_output = true}; + auto solid_system = buildSolidMechanicsSystem(solid_nonlinear_opts, solid_linear_options, solid_opts, + mesh, FieldType("bulk"), + FieldType("shear")); static constexpr double gravity = -9.0; @@ -110,47 +189,51 @@ TEST_F(SolidMechanicsMeshFixture, TransientConstantGravity) double nu = 0.25; auto K = E / (3.0 * (1.0 - 2.0 * nu)); auto G = E / (2.0 * (1.0 + nu)); - using MaterialType = solid_mechanics::ParameterizedNeoHookeanSolid; - MaterialType material{.density = 1.0, .K0 = K, .G0 = G}; + solid_mechanics::ParameterizedNeoHookeanSolid material_base{.density = 1.0, .K0 = K, .G0 = G}; + auto material = makeTimeInfoMaterial(material_base); // Set parameters - auto params = system.getParameterFields(); - params[0].get()->setFromFieldFunction([=](tensor) { return material.K0; }); - params[1].get()->setFromFieldFunction([=](tensor) { return material.G0; }); + auto params = solid_system->field_store->getParameterFields(); + params[0].get()->setFromFieldFunction([=](tensor) { return material_base.K0; }); + params[1].get()->setFromFieldFunction([=](tensor) { return material_base.G0; }); - system.setMaterial(material, mesh->entireBodyName()); + solid_system->setMaterial(material, mesh->entireBodyName()); // Add gravity body force - system.addBodyForce(mesh->entireBodyName(), - [](double /*time*/, auto /*X*/, auto /*u*/, auto /*v*/, auto /*a*/, auto... /*params*/) { - tensor b{}; - b[1] = gravity; - return b; - }); + solid_system->addBodyForce(mesh->entireBodyName(), + [](double /*time*/, auto /*X*/, auto /*u*/, auto /*v*/, auto /*a*/, auto... /*params*/) { + tensor b{}; + b[1] = gravity; + return b; + }); // Add dummy traction to test compilation - system.addTraction("right", [](double /*time*/, auto /*X*/, auto /*n*/, auto /*u*/, auto /*v*/, auto /*a*/, - auto... /*params*/) { return tensor{}; }); + solid_system->addTraction("right", [](double /*time*/, auto /*X*/, auto /*n*/, auto /*u*/, auto /*v*/, auto /*a*/, + auto... /*params*/) { return tensor{}; }); - auto shape_disp = system.field_store->getShapeDisp(); - auto states = system.getStateFields(); - auto output_states = system.getOutputFieldStates(); + auto shape_disp = solid_system->field_store->getShapeDisp(); + auto states = solid_system->field_store->getStateFields(); + auto output_states = solid_system->field_store->getOutputFieldStates(); std::string pv_dir = "paraview_solid"; - auto pv_writer = createParaviewWriter(*mesh, {output_states[0], params[0], params[1]}, pv_dir); - pv_writer.write(0, 0.0, {output_states[0], params[0], params[1]}); + auto pv_fields = std::vector{output_states[0], params[0], params[1]}; + auto pv_writer = createParaviewWriter(*mesh, pv_fields, pv_dir); + pv_writer.write(0, 0.0, pv_fields); double time = 0.0; size_t cycle = 0; std::vector reactions; + auto advancer = makeAdvancer(solid_system); + for (size_t m = 0; m < num_steps_; ++m) { TimeInfo t_info(time, dt_, cycle); - std::tie(states, reactions) = system.advancer->advanceState(t_info, shape_disp, states, params); - output_states = system.getOutputFieldStates(); + std::tie(states, reactions) = advancer->advanceState(t_info, shape_disp, states, params); + output_states = solid_system->field_store->getOutputFieldStates(); + pv_fields = {output_states[0], params[0], params[1]}; time += dt_; cycle++; - pv_writer.write(m + 1, time, {output_states[0], params[0], params[1]}); + pv_writer.write(m + 1, time, pv_fields); } double a_exact = gravity; @@ -161,29 +244,29 @@ TEST_F(SolidMechanicsMeshFixture, TransientConstantGravity) // Test acceleration (states[3] is acceleration) FunctionalObjective> accel_error("accel_error", mesh, spaces({states[3]})); - accel_error.addBodyIntegral(DependsOn<0>{}, mesh->entireBodyName(), [a_exact](auto /*t*/, auto /*X*/, auto A) { + accel_error.addBodyIntegral(mesh->entireBodyName(), [a_exact](auto /*t*/, auto /*X*/, auto A) { auto a = get(A); auto da0 = a[0]; auto da1 = a[1] - a_exact; return da0 * da0 + da1 * da1; }); double a_err = accel_error.evaluate(endTimeInfo, shape_disp.get().get(), getConstFieldPointers({states[3]})); - EXPECT_NEAR(0.0, a_err, 1e-14); + EXPECT_NEAR(0.0, a_err, 1e-12); // Test velocity (states[2] is velocity) FunctionalObjective> velo_error("velo_error", mesh, spaces({states[2]})); - velo_error.addBodyIntegral(DependsOn<0>{}, mesh->entireBodyName(), [v_exact](auto /*t*/, auto /*X*/, auto V) { + velo_error.addBodyIntegral(mesh->entireBodyName(), [v_exact](auto /*t*/, auto /*X*/, auto V) { auto v = get(V); auto dv0 = v[0]; auto dv1 = v[1] - v_exact; return dv0 * dv0 + dv1 * dv1; }); double v_err = velo_error.evaluate(TimeInfo(0.0, 1.0, 0), shape_disp.get().get(), getConstFieldPointers({states[2]})); - EXPECT_NEAR(0.0, v_err, 1e-14); + EXPECT_NEAR(0.0, v_err, 1e-12); // Test displacement (states[1] is displacement) FunctionalObjective> disp_error("disp_error", mesh, spaces({states[1]})); - disp_error.addBodyIntegral(DependsOn<0>{}, mesh->entireBodyName(), [u_exact](auto /*t*/, auto /*X*/, auto U) { + disp_error.addBodyIntegral(mesh->entireBodyName(), [u_exact](auto /*t*/, auto /*X*/, auto U) { auto u = get(U); auto du0 = u[0]; auto du1 = u[1] - u_exact; @@ -193,20 +276,78 @@ TEST_F(SolidMechanicsMeshFixture, TransientConstantGravity) EXPECT_NEAR(0.0, u_err, 1e-14); } -auto createSolidMechanicsBasePhysics(std::string physics_name, std::shared_ptr mesh) +TEST_F(SolidMechanicsMeshFixture, TransientFreefallWithConsistentBoundaryConditions) { - std::shared_ptr solid_block_solver = - buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + SolidMechanicsOptions solid_options{.enable_stress_output = true}; + auto solid_system = + buildSolidMechanicsSystem(solid_nonlinear_opts, solid_linear_options, solid_options, mesh); + + static constexpr double gravity = -9.0; + double E = 100.0; + double nu = 0.25; + auto K = E / (3.0 * (1.0 - 2.0 * nu)); + auto G = E / (2.0 * (1.0 + nu)); + solid_system->setMaterial(makeTimeInfoMaterial(solid_mechanics::NeoHookean{.density = 1.0, .K = K, .G = G}), + mesh->entireBodyName()); + + solid_system->addBodyForce(mesh->entireBodyName(), + [](double /*time*/, auto /*X*/, auto /*u*/, auto /*v*/, auto /*a*/) { + tensor b{}; + b[1] = gravity; + return b; + }); + + solid_system->disp_bc->setVectorBCs(mesh->entireBoundary(), [](double t, tensor /*X*/) { + tensor u{}; + u[1] = 0.5 * gravity * t * t; + return u; + }); + + ASSERT_FALSE(solid_system->cycle_zero_systems.empty()); - auto time_rule = ImplicitNewmarkSecondOrderTimeIntegrationRule(); + const double dt = 1.0e-3; + const size_t num_steps = 4; + + auto physics = makeDifferentiablePhysics(solid_system, "freefall"); + for (size_t step = 0; step < num_steps; ++step) { + physics->advanceTimestep(dt); + } + + auto states = physics->getFieldStates(); + auto shape_disp = physics->getShapeDispFieldState(); + double time = num_steps * dt; + auto vector_error = [&](const std::string& name, const FieldState& state, double y_exact) { + auto state_vec = std::vector{state}; + FunctionalObjective> error(name, mesh, spaces(state_vec)); + error.addBodyIntegral(mesh->entireBodyName(), [y_exact](auto /*t*/, auto /*X*/, auto U) { + auto u = get(U); + return u[0] * u[0] + (u[1] - y_exact) * (u[1] - y_exact) + u[2] * u[2]; + }); + return error.evaluate(TimeInfo(time, dt, 0), shape_disp.get().get(), getConstFieldPointers(state_vec)); + }; + + double u_exact = 0.5 * gravity * time * time; + double v_exact = gravity * time; + EXPECT_NEAR(0.0, vector_error("freefall_displacement_error", states[0], u_exact), 1e-12); + EXPECT_NEAR(0.0, vector_error("freefall_velocity_error", states[2], v_exact), 1e-11); + EXPECT_NEAR(0.0, vector_error("freefall_acceleration_error", states[3], gravity), 1e-6); +} + +auto createSolidMechanicsBasePhysics(std::string physics_name, std::shared_ptr mesh, + const NonlinearSolverOptions& nonlinear_opts, + const LinearSolverOptions& linear_opts) +{ + auto field_store = std::make_shared(mesh, 100, physics_name); - auto coupled_solver = std::make_shared(solid_block_solver); - auto system = buildSolidMechanicsSystem(mesh, coupled_solver, time_rule, physics_name, - FieldType("bulk"), - FieldType("shear")); + auto param_fields = registerParameterFields(field_store, FieldType("bulk"), + FieldType("shear")); + auto solid_fields = + registerSolidMechanicsFields(field_store); + auto solver = std::make_shared(buildNonlinearBlockSolver(nonlinear_opts, linear_opts, *mesh)); + auto solid_system = buildSolidMechanicsSystem(solver, SolidMechanicsOptions{}, solid_fields, param_fields); - auto physics = system.createDifferentiablePhysics(physics_name); - auto bcs = system.disp_bc; + auto physics = makeDifferentiablePhysics(solid_system, physics_name); + auto bcs = solid_system->disp_bc; bcs->setFixedVectorBCs(mesh->domain("right")); bcs->setVectorBCs(mesh->domain("left"), [](double t, tensor X) { @@ -220,10 +361,10 @@ auto createSolidMechanicsBasePhysics(std::string physics_name, std::shared_ptrentireBodyName()); + solid_system->setMaterial(material, mesh->entireBodyName()); auto shape_disp = physics->getShapeDispFieldState(); auto params = physics->getFieldParams(); @@ -231,12 +372,12 @@ auto createSolidMechanicsBasePhysics(std::string physics_name, std::shared_ptrsetFromFieldFunction([=](tensor) { double scaling = 1.0; - return scaling * material.K0; + return scaling * material_base.K0; }); params[1].get()->setFromFieldFunction([=](tensor) { double scaling = 1.0; - return scaling * material.G0; + return scaling * material_base.G0; }); physics->resetStates(); @@ -248,7 +389,8 @@ TEST_F(SolidMechanicsMeshFixture, SensitivitiesGretl) { SMITH_MARK_FUNCTION; std::string physics_name = "solid"; - auto [physics, shape_disp, initial_states, params, bcs] = createSolidMechanicsBasePhysics(physics_name, mesh); + auto [physics, shape_disp, initial_states, params, bcs] = + createSolidMechanicsBasePhysics(physics_name, mesh, solid_nonlinear_opts, solid_linear_options); auto pv_writer = smith::createParaviewWriter(*mesh, physics->getFieldStatesAndParamStates(), physics_name); pv_writer.write(0, physics->time(), physics->getFieldStatesAndParamStates()); @@ -327,7 +469,8 @@ TEST_F(SolidMechanicsMeshFixture, SensitivitiesBasePhysics) { SMITH_MARK_FUNCTION; std::string physics_name = "solid"; - auto [physics, shape_disp, initial_states, params, bcs] = createSolidMechanicsBasePhysics(physics_name, mesh); + auto [physics, shape_disp, initial_states, params, bcs] = + createSolidMechanicsBasePhysics(physics_name, mesh, solid_nonlinear_opts, solid_linear_options); double qoi = integrateForward(*physics, num_steps_, dt_, physics_name + "_reactions"); SLIC_INFO_ROOT(axom::fmt::format("{}", qoi)); @@ -366,7 +509,7 @@ TEST_F(SolidMechanicsMeshFixture, SensitivitiesComparison) // 1. Calculate sensitivities using Gretl auto [physicsGretl, shape_dispG, initial_statesG, paramsG, bcsG] = - createSolidMechanicsBasePhysics(physics_name + "_gretl", mesh); + createSolidMechanicsBasePhysics(physics_name + "_gretl", mesh, solid_nonlinear_opts, solid_linear_options); // Forward pass for (size_t m = 0; m < num_steps_; ++m) { @@ -382,7 +525,7 @@ TEST_F(SolidMechanicsMeshFixture, SensitivitiesComparison) // 2. Calculate sensitivities using BasePhysics manual adjoint auto [physicsBase, shape_dispB, initial_statesB, paramsB, bcsB] = - createSolidMechanicsBasePhysics(physics_name + "_base", mesh); + createSolidMechanicsBasePhysics(physics_name + "_base", mesh, solid_nonlinear_opts, solid_linear_options); // Forward pass double qoiB = integrateForward(*physicsBase, num_steps_, dt_, physics_name + "_base_reactions"); @@ -425,7 +568,7 @@ TEST_F(SolidMechanicsMeshFixture, SensitivitiesComparison) } // Compare initial condition sensitivities - std::vector state_suffixes = {"displacement_solve_state", "displacement", "velocity", "acceleration"}; + std::vector state_suffixes = {"displacement", "velocity", "acceleration"}; for (const auto& suffix : state_suffixes) { std::string nameG = physics_name + "_gretl_" + suffix; std::string nameB = physics_name + "_base_" + suffix; diff --git a/src/smith/differentiable_numerics/tests/test_solid_static_with_internal_vars.cpp b/src/smith/differentiable_numerics/tests/test_solid_static_with_internal_vars.cpp index 9ada65a4ec..d4a085c238 100644 --- a/src/smith/differentiable_numerics/tests/test_solid_static_with_internal_vars.cpp +++ b/src/smith/differentiable_numerics/tests/test_solid_static_with_internal_vars.cpp @@ -9,32 +9,37 @@ #include "smith/infrastructure/application_manager.hpp" #include "smith/numerics/solver_config.hpp" #include "smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/state_variable_system.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" #include "smith/differentiable_numerics/differentiable_test_utils.hpp" #include "smith/differentiable_numerics/paraview_writer.hpp" namespace smith { -LinearSolverOptions solid_linear_options{.linear_solver = LinearSolver::SuperLU, - .preconditioner = Preconditioner::None, - .relative_tol = 1e-12, - .absolute_tol = 1e-12, - .max_iterations = 2000, - .print_level = 0}; - -NonlinearSolverOptions solid_nonlinear_opts{.nonlin_solver = NonlinearSolver::NewtonLineSearch, - .relative_tol = 1.0e-10, - .absolute_tol = 1.0e-10, - .max_iterations = 100, - .max_line_search_iterations = 50, - .print_level = 1}; - static constexpr int dim = 3; static constexpr int disp_order = 1; static constexpr int state_order = 0; using StateSpace = L2; +using DispRule = QuasiStaticSecondOrderTimeIntegrationRule; +using InternalVariableRule = BackwardEulerFirstOrderTimeIntegrationRule; struct SolidStaticWithInternalVarsFixture : public testing::Test { + LinearSolverOptions solid_linear_options{.linear_solver = LinearSolver::SuperLU, + .preconditioner = Preconditioner::None, + .relative_tol = 1e-12, + .absolute_tol = 1e-12, + .max_iterations = 2000, + .print_level = 0}; + + NonlinearSolverOptions solid_nonlinear_opts{.nonlin_solver = NonlinearSolver::NewtonLineSearch, + .relative_tol = 1.0e-10, + .absolute_tol = 1.0e-10, + .max_iterations = 100, + .max_line_search_iterations = 50, + .print_level = 1}; + void SetUp() override { StateManager::initialize(datastore, "solid_static_with_internal_vars"); @@ -56,8 +61,9 @@ struct DamageMaterial { double nu = 0.3; double density = 1.0; - template - SMITH_HOST_DEVICE auto operator()(StateType /*state*/, DerivType deriv_u, ISVType isv, Params... /*params*/) const + template + SMITH_HOST_DEVICE auto operator()(const TimeInfo& /*t_info*/, StateType /*state*/, DerivType deriv_u, + GradVType /*grad_v*/, ISVType isv, Params... /*params*/) const { auto epsilon = sym(deriv_u); auto tr_eps = tr(epsilon); @@ -93,78 +99,102 @@ struct StrainNormEvolution { using std::sqrt; auto epsilon = sym(deriv_u); auto eps_norm = sqrt(inner(epsilon, epsilon) + 1e-16); - - // ODE: isv_dot = (1 - isv) * eps_norm return isv_dot - (1.0 - isv) * eps_norm; } }; -TEST_F(SolidStaticWithInternalVarsFixture, CoupledSolve) +std::shared_ptr makeSystemSolver(const NonlinearSolverOptions& nonlinear_opts, + const LinearSolverOptions& linear_opts, const Mesh& mesh) { - auto nonlinear_block_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); - - auto coupled_solver = std::make_shared(nonlinear_block_solver); - auto system = buildSolidMechanicsWithInternalVarsSystem( - mesh, coupled_solver, QuasiStaticSecondOrderTimeIntegrationRule{}, BackwardEulerFirstOrderTimeIntegrationRule{}, - "solid_static_with_internal_vars"); - - // Material and Evolution - system.setMaterial(DamageMaterial{}, mesh->entireBodyName()); - system.addStateEvolution(mesh->entireBodyName(), StrainNormEvolution{}); + return std::make_shared(buildNonlinearBlockSolver(nonlinear_opts, linear_opts, mesh)); +} - // Boundary Conditions +auto registerFields(const std::shared_ptr& field_store) +{ + return std::tuple{registerSolidMechanicsFields(field_store), + registerInternalVariableFields(field_store)}; +} - // Fix bottom face - system.disp_bc->setFixedVectorBCs(mesh->domain("bottom")); +template +void setDamageCoupling(const std::shared_ptr& solid_system, + const std::shared_ptr& internal_variable_system, + const std::shared_ptr& mesh) +{ + setCoupledSolidMechanicsInternalVariableMaterial(solid_system, internal_variable_system, DamageMaterial{}, + mesh->entireBodyName()); + setCoupledInternalVariableMaterial(internal_variable_system, solid_system, StrainNormEvolution{}, + mesh->entireBodyName()); +} - // Pull top face - double pull_rate = 0.05; - system.disp_bc->setVectorBCs(mesh->domain("top"), [pull_rate](double t, tensor /*X*/) { +template +void setPullBoundaryConditions(const std::shared_ptr& solid_system, const std::shared_ptr& mesh, + double pull_rate) +{ + solid_system->setDisplacementBC(mesh->domain("bottom")); + solid_system->setDisplacementBC(mesh->domain("top"), [pull_rate](double t, tensor /*X*/) { tensor u{}; u[2] = pull_rate * t; return u; }); +} + +TEST_F(SolidStaticWithInternalVarsFixture, CoupledSolve) +{ + auto solid_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto internal_variable_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto nonlinear_block_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto coupled_solver = std::make_shared(nonlinear_block_solver); + auto field_store = std::make_shared(mesh, 100, "solid_static_with_internal_vars_"); + auto [solid_fields, internal_variable_fields] = registerFields(field_store); + auto solid_system = buildSolidMechanicsSystem(solid_solver, SolidMechanicsOptions{}, solid_fields, + couplingFields(internal_variable_fields)); + auto internal_variable_system = + buildInternalVariableSystem(internal_variable_solver, internal_variable_fields, couplingFields(solid_fields)); + + auto system = combineSystems(coupled_solver, solid_system, internal_variable_system); - auto physics = system.createDifferentiablePhysics("physics"); + setDamageCoupling(solid_system, internal_variable_system, mesh); + setPullBoundaryConditions(solid_system, mesh, 0.05); + + auto physics = makeDifferentiablePhysics(system, "physics"); // Create ParaView writer - auto writer = createParaviewWriter(*mesh, system.getOutputFieldStates(), "solid_state_output"); - writer.write(0, 0.0, system.getOutputFieldStates()); + auto writer = createParaviewWriter(*mesh, system->field_store->getOutputFieldStates(), "solid_state_output"); + writer.write(0, 0.0, system->field_store->getOutputFieldStates()); // Advance multiple steps for (int step = 1; step <= 5; ++step) { physics->advanceTimestep(1.0); - writer.write(step, step * 1.0, system.getOutputFieldStates()); + writer.write(step, step * 1.0, system->field_store->getOutputFieldStates()); SLIC_INFO("Completed step " << step); } } TEST_F(SolidStaticWithInternalVarsFixture, StaggeredSolveWithRelaxation) { + auto solid_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto internal_variable_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); auto disp_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); - auto state_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto internal_variable_block_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); - // Staggered solver: stage 0 solves displacement (block 0), stage 1 solves state (block 1). + // Staggered solver: stage 0 solves displacement, stage 1 solves internal variable. // Use relaxation_factor = 0.5 on the displacement stage to exercise the relaxation path. - auto staggered_solver = std::make_shared(20); + auto staggered_solver = std::make_shared(20); staggered_solver->addSubsystemSolver({0}, disp_solver, 0.5); - staggered_solver->addSubsystemSolver({1}, state_solver, 1.0); + staggered_solver->addSubsystemSolver({1}, internal_variable_block_solver, 1.0); - auto system = buildSolidMechanicsWithInternalVarsSystem( - mesh, staggered_solver, QuasiStaticSecondOrderTimeIntegrationRule{}, BackwardEulerFirstOrderTimeIntegrationRule{}, - "solid_staggered_relaxation"); + auto field_store = std::make_shared(mesh, 100, "solid_staggered_relaxation_"); + auto [solid_fields, internal_variable_fields] = registerFields(field_store); + auto solid_system = buildSolidMechanicsSystem(solid_solver, SolidMechanicsOptions{}, solid_fields, + couplingFields(internal_variable_fields)); + auto internal_variable_system = + buildInternalVariableSystem(internal_variable_solver, internal_variable_fields, couplingFields(solid_fields)); - system.setMaterial(DamageMaterial{}, mesh->entireBodyName()); - system.addStateEvolution(mesh->entireBodyName(), StrainNormEvolution{}); + auto system = combineSystems(staggered_solver, solid_system, internal_variable_system); - system.disp_bc->setFixedVectorBCs(mesh->domain("bottom")); - double pull_rate = 0.05; - system.disp_bc->setVectorBCs(mesh->domain("top"), [pull_rate](double t, tensor /*X*/) { - tensor u{}; - u[2] = pull_rate * t; - return u; - }); + setDamageCoupling(solid_system, internal_variable_system, mesh); + setPullBoundaryConditions(solid_system, mesh, 0.05); - auto physics = system.createDifferentiablePhysics("physics_relaxed"); + auto physics = makeDifferentiablePhysics(system, "physics_relaxed"); for (int step = 1; step <= 3; ++step) { physics->advanceTimestep(1.0); SLIC_INFO("Staggered relaxation step " << step << " completed"); @@ -173,21 +203,27 @@ TEST_F(SolidStaticWithInternalVarsFixture, StaggeredSolveWithRelaxation) TEST_F(SolidStaticWithInternalVarsFixture, BodyForceAndTraction) { + auto solid_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); + auto internal_variable_solver = makeSystemSolver(solid_nonlinear_opts, solid_linear_options, *mesh); auto nonlinear_block_solver = buildNonlinearBlockSolver(solid_nonlinear_opts, solid_linear_options, *mesh); - auto coupled_solver = std::make_shared(nonlinear_block_solver); - auto system = buildSolidMechanicsWithInternalVarsSystem( - mesh, coupled_solver, QuasiStaticSecondOrderTimeIntegrationRule{}, BackwardEulerFirstOrderTimeIntegrationRule{}, - "body_force_test"); + auto coupled_solver = std::make_shared(nonlinear_block_solver); + auto field_store = std::make_shared(mesh, 100, "body_force_test_"); + auto [solid_fields, internal_variable_fields] = registerFields(field_store); + auto solid_system = buildSolidMechanicsSystem(solid_solver, SolidMechanicsOptions{}, solid_fields, + couplingFields(internal_variable_fields)); + auto internal_variable_system = + buildInternalVariableSystem(internal_variable_solver, internal_variable_fields, couplingFields(solid_fields)); + + auto system = combineSystems(coupled_solver, solid_system, internal_variable_system); - system.setMaterial(DamageMaterial{}, mesh->entireBodyName()); - system.addStateEvolution(mesh->entireBodyName(), StrainNormEvolution{}); + setDamageCoupling(solid_system, internal_variable_system, mesh); // Fix bottom face - system.disp_bc->setFixedVectorBCs(mesh->domain("bottom")); + solid_system->setDisplacementBC(mesh->domain("bottom")); // Apply a gravity-like body force in the -z direction double body_force_mag = -0.01; - system.addBodyForce(mesh->entireBodyName(), [=](double, auto, auto, auto, auto, auto, auto) { + solid_system->addBodyForce(mesh->entireBodyName(), [=](double, auto, auto, auto, auto, auto, auto, auto... /*args*/) { tensor f{}; f[2] = body_force_mag; return f; @@ -195,13 +231,13 @@ TEST_F(SolidStaticWithInternalVarsFixture, BodyForceAndTraction) // Apply a traction on the top face in the +z direction double traction_mag = 0.005; - system.addTraction("top", [=](double, auto, auto /*n*/, auto, auto, auto, auto, auto) { + solid_system->addTraction("top", [=](double, auto, auto /*n*/, auto, auto, auto, auto, auto, auto... /*args*/) { tensor t{}; t[2] = traction_mag; return t; }); - auto physics = system.createDifferentiablePhysics("physics_bf"); + auto physics = makeDifferentiablePhysics(system, "physics_bf"); physics->advanceTimestep(1.0); // Check that the displacement field is non-zero (the body force + traction produced deformation) diff --git a/src/smith/differentiable_numerics/tests/test_thermal_static.cpp b/src/smith/differentiable_numerics/tests/test_thermal_static.cpp index 0381cd4f21..6ad9936af1 100644 --- a/src/smith/differentiable_numerics/tests/test_thermal_static.cpp +++ b/src/smith/differentiable_numerics/tests/test_thermal_static.cpp @@ -7,18 +7,12 @@ #include #include #include "smith/differentiable_numerics/thermal_system.hpp" -#include "smith/differentiable_numerics/nonlinear_solve.hpp" #include "smith/differentiable_numerics/nonlinear_block_solver.hpp" -#include "smith/differentiable_numerics/coupled_system_solver.hpp" #include "smith/physics/mesh.hpp" #include "smith/physics/common.hpp" #include "smith/physics/state/state_manager.hpp" -#include "smith/numerics/functional/functional.hpp" #include "smith/numerics/solver_config.hpp" #include "smith/infrastructure/application_manager.hpp" -#include "axom/slic.hpp" -#include "axom/fmt.hpp" -#include "axom/sidre.hpp" using namespace smith; @@ -48,38 +42,36 @@ struct ThermalStaticFixture : public testing::Test { auto solver_options = NonlinearSolverOptions(); solver_options.relative_tol = 1e-12; auto linear_options = LinearSolverOptions(); - auto nonlinear_block_solver = buildNonlinearBlockSolver(solver_options, linear_options, *mesh); - auto coupled_solver = std::make_shared(nonlinear_block_solver); - auto thermal_system = buildThermalSystem<2, temp_order>(mesh, coupled_solver); + auto thermal_system = buildThermalSystem<2, temp_order>(solver_options, linear_options, ThermalOptions{}, mesh); double k = 1.0; // Material returns {heat_capacity, heat_flux} consistent with heat_transfer.hpp convention. // heat_flux is the physical flux (Fourier's law): q = -k * grad(T). // The system negates it to form the weak form integral(k * grad(T) . grad(v)). - thermal_system.setMaterial( - [=](auto /*temperature*/, auto grad_temperature) { return smith::tuple{0.0, -k * grad_temperature}; }, - "entire_body"); + thermal_system->setMaterial([=](const TimeInfo& /*t_info*/, auto /*temperature*/, + auto grad_temperature) { return smith::tuple{0.0, -k * grad_temperature}; }, + "entire_body"); - thermal_system.addHeatSource("entire_body", [=](auto /*t*/, auto X, auto /*T*/) { + thermal_system->addHeatSource("entire_body", [=](auto /*t*/, auto X, auto /*T*/, auto... /*args*/) { auto x = X[0]; auto y = X[1]; return 2.0 * k * M_PI * M_PI * sin(M_PI * x) * sin(M_PI * y); }); - thermal_system.temperature_bc->template setScalarBCs<2>(mesh->entireBoundary(), - [](double /*t*/, tensor /*X*/) { return 0.0; }); + thermal_system->setTemperatureBC(mesh->entireBoundary(), [](double /*t*/, tensor /*X*/) { return 0.0; }); TimeInfo t_info(0.0, 1.0); - auto [new_states, reactions] = thermal_system.advancer->advanceState( - t_info, thermal_system.field_store->getShapeDisp(), thermal_system.field_store->getAllFields(), - thermal_system.getParameterFields()); + auto [new_states, reactions] = makeAdvancer(thermal_system) + ->advanceState(t_info, thermal_system->field_store->getShapeDisp(), + thermal_system->field_store->getAllFields(), + thermal_system->field_store->getParameterFields()); for (size_t i = 0; i < new_states.size(); ++i) { - thermal_system.field_store->setField(i, new_states[i]); + thermal_system->field_store->setField(i, new_states[i]); } - auto temperature = thermal_system.field_store->getField(thermal_system.prefix("temperature")); + auto temperature = thermal_system->field_store->getField("temperature"); auto exact_sol_func = [](const mfem::Vector& X, mfem::Vector& T) { double x = X(0); @@ -136,45 +128,42 @@ TEST_F(ThermalStaticFixture, HeatSourceWithDependsOn) auto solver_options = NonlinearSolverOptions(); solver_options.relative_tol = 1e-12; auto linear_options = LinearSolverOptions(); - auto nonlinear_block_solver = buildNonlinearBlockSolver(solver_options, linear_options, *mesh); - auto coupled_solver = std::make_shared(nonlinear_block_solver); - FieldType> conductivity_param("conductivity"); - auto thermal_system = buildThermalSystem<2, 1>(mesh, coupled_solver, QuasiStaticFirstOrderTimeIntegrationRule{}, "", - conductivity_param); + auto thermal_system = buildThermalSystem<2, 1>(solver_options, linear_options, ThermalOptions{}, mesh, + FieldType>("conductivity")); // Set the conductivity parameter field to k=1.0 - thermal_system.parameter_fields[0].get()->setFromFieldFunction([](tensor) { return 1.0; }); + thermal_system->field_store->getParameterFields()[0].get()->setFromFieldFunction( + [](tensor) { return 1.0; }); // Material uses the parameter field for conductivity - thermal_system.setMaterial( - [](auto /*temperature*/, auto grad_temperature, auto k_param) { + thermal_system->setMaterial( + [](const TimeInfo& /*t_info*/, auto /*temperature*/, auto grad_temperature, auto k_param) { auto k = get<0>(k_param); return smith::tuple{0.0, -k * grad_temperature}; }, "entire_body"); - // Use DependsOn to specify that the heat source depends only on the temperature states (indices 0,1), - // not on the parameter field - thermal_system.addHeatSource(DependsOn<0, 1>{}, "entire_body", [=](auto /*t*/, auto X, auto /*T*/) { + // DependsOn now indexes only trailing coupling/parameter args, so empty means "state-only". + thermal_system->addHeatSource("entire_body", [=](auto /*t*/, auto X, auto /*T*/, auto... /*args*/) { auto x = X[0]; auto y = X[1]; return 2.0 * M_PI * M_PI * sin(M_PI * x) * sin(M_PI * y); }); - thermal_system.temperature_bc->template setScalarBCs<2>(mesh->entireBoundary(), - [](double /*t*/, tensor /*X*/) { return 0.0; }); + thermal_system->setTemperatureBC(mesh->entireBoundary(), [](double /*t*/, tensor /*X*/) { return 0.0; }); TimeInfo t_info(0.0, 1.0); - auto [new_states, reactions] = thermal_system.advancer->advanceState( - t_info, thermal_system.field_store->getShapeDisp(), thermal_system.field_store->getAllFields(), - thermal_system.getParameterFields()); + auto [new_states, reactions] = makeAdvancer(thermal_system) + ->advanceState(t_info, thermal_system->field_store->getShapeDisp(), + thermal_system->field_store->getAllFields(), + thermal_system->field_store->getParameterFields()); for (size_t i = 0; i < new_states.size(); ++i) { - thermal_system.field_store->setField(i, new_states[i]); + thermal_system->field_store->setField(i, new_states[i]); } - auto temperature = thermal_system.field_store->getField(thermal_system.prefix("temperature")); + auto temperature = thermal_system->field_store->getField("temperature"); auto exact_sol_func = [](const mfem::Vector& X, mfem::Vector& T) { double x = X(0); diff --git a/src/smith/differentiable_numerics/tests/test_thermo_mechanics_with_internal_vars.cpp b/src/smith/differentiable_numerics/tests/test_thermo_mechanics_with_internal_vars.cpp new file mode 100644 index 0000000000..8ca47787f8 --- /dev/null +++ b/src/smith/differentiable_numerics/tests/test_thermo_mechanics_with_internal_vars.cpp @@ -0,0 +1,232 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +/** + * @file test_thermo_mechanics_with_internal_vars.cpp + * @brief Tests thermo-mechanics with internal variables using three composed systems. + * + * Validates N>2 system coupling end-to-end via combineSystems. + * + * Layout: + * - Solid: receives temperature and alpha coupling. + * - Thermal: standalone heat diffusion with a heat source. + * - Internal variable (damage): receives solid displacement coupling; damage evolves with strain norm. + */ + +#include +#include "gtest/gtest.h" + +#include "smith/smith_config.hpp" +#include "smith/infrastructure/application_manager.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/physics/state/state_manager.hpp" +#include "smith/physics/mesh.hpp" + +#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" +#include "smith/differentiable_numerics/system_solver.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_with_internal_vars_system.hpp" +#include "smith/differentiable_numerics/state_variable_system.hpp" +#include "smith/differentiable_numerics/combined_system.hpp" +#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" +#include "smith/differentiable_numerics/differentiable_test_utils.hpp" + +namespace smith { + +static constexpr int dim = 3; +static constexpr int disp_ord = 1; +static constexpr int temp_ord = 1; +static constexpr int state_ord = 0; +using StateSpace = L2; + +struct SimpleThermoelasticMaterial { + using State = smith::QOI; + + double density = 1.0; + double E = 100.0; + double nu = 0.25; + double alpha_th = 0.001; + double kappa = 0.05; + double heat_capacity = 1.0; + double heat_source = 0.5; + + template + SMITH_HOST_DEVICE auto effectiveYoungsModulus(AlphaType alpha) const + { + return E * (1.0 - 0.8 * alpha); + } + + template + SMITH_HOST_DEVICE auto operator()(const TimeInfo& /*t_info*/, StateType /*state*/, GradUType grad_u, + GradVType /*grad_v*/, ThetaType theta, GradThetaType grad_theta, AlphaType alpha, + Params... /*params*/) const + { + auto E_eff = effectiveYoungsModulus(alpha); + auto lam = E_eff * nu / ((1.0 + nu) * (1.0 - 2.0 * nu)); + auto mu = E_eff / (2.0 * (1.0 + nu)); + static constexpr auto I = Identity(); + auto eps = sym(grad_u); + auto pk = lam * (tr(eps) - 3.0 * alpha_th * theta) * I + 2.0 * mu * eps; + auto q0 = kappa * grad_theta; + return smith::tuple{pk, heat_capacity, heat_source, q0}; + } + + template + SMITH_HOST_DEVICE auto operator()(const TimeInfo& t_info, StateType state, GradUType grad_u, GradVType grad_v, + ThetaType theta, GradThetaType grad_theta, + smith::tuple alpha, Params... params) const + { + return (*this)(t_info, state, grad_u, grad_v, theta, grad_theta, smith::get(alpha), params...); + } + + template + SMITH_HOST_DEVICE auto operator()(const TimeInfo& t_info, StateType state, GradUType grad_u, GradVType grad_v, + ThetaType theta, GradThetaType grad_theta, Params... params) const + { + return (*this)(t_info, state, grad_u, grad_v, theta, grad_theta, 0.0, params...); + } +}; + +struct StrainDrivenInternalVariableMaterial { + template + SMITH_HOST_DEVICE auto operator()(TimeInfo /*t_info*/, AlphaType alpha, AlphaDotType alpha_dot, DerivType grad_u, + Params... /*params*/) const + { + using std::sqrt; + auto eps = sym(grad_u); + auto eps_norm = sqrt(inner(eps, eps) + 1e-16); + return alpha_dot - (1.0 - alpha) * eps_norm; + } +}; + +struct ThreeSystemMeshFixture : public testing::Test { + void SetUp() override + { + datastore_ = std::make_unique(); + smith::StateManager::initialize(*datastore_, "thermo_mechanics_with_internal_vars"); + mesh_ = std::make_shared( + mfem::Mesh::MakeCartesian3D(4, 2, 2, mfem::Element::HEXAHEDRON, 1.0, 0.1, 0.1), "mesh", 0, 0); + mesh_->addDomainOfBoundaryElements("left", smith::by_attr(3)); + mesh_->addDomainOfBoundaryElements("right", smith::by_attr(5)); + } + + std::unique_ptr datastore_; + std::shared_ptr mesh_; +}; + +TEST_F(ThreeSystemMeshFixture, StronglyCoupledThreeSystems) +{ + smith::LinearSolverOptions lin_opts{.linear_solver = smith::LinearSolver::SuperLU, + .relative_tol = 1e-8, + .absolute_tol = 1e-10, + .max_iterations = 200, + .print_level = 0}; + smith::NonlinearSolverOptions nonlin_opts{.nonlin_solver = smith::NonlinearSolver::NewtonLineSearch, + .relative_tol = 1e-7, + .absolute_tol = 1e-8, + .max_iterations = 20, + .max_line_search_iterations = 6, + .print_level = 0}; + + auto solid_block_solver = buildNonlinearBlockSolver(nonlin_opts, lin_opts, *mesh_); + auto thermal_block_solver = buildNonlinearBlockSolver(nonlin_opts, lin_opts, *mesh_); + auto internal_variable_block_solver = buildNonlinearBlockSolver(nonlin_opts, lin_opts, *mesh_); + + auto field_store = std::make_shared(mesh_, 100); + + using DispRule = QuasiStaticSecondOrderTimeIntegrationRule; + using TempRule = BackwardEulerFirstOrderTimeIntegrationRule; + using InternalVariableRule = BackwardEulerFirstOrderTimeIntegrationRule; + + // Phase 1: register all fields. + // registerSolidMechanicsFields must come before registerThermalFields to get + // the displacement field tokens available for thermal coupling. + auto solid_fields = registerSolidMechanicsFields(field_store); + auto thermal_fields = registerThermalFields(field_store); + auto internal_variable_fields = registerInternalVariableFields(field_store); + + // Phase 2: build each system. + // Solid receives thermal and alpha coupling. + auto solid_system = + buildSolidMechanicsSystem(std::make_shared(solid_block_solver), SolidMechanicsOptions{}, + solid_fields, couplingFields(thermal_fields, internal_variable_fields)); + + auto thermal_system = buildThermalSystem(std::make_shared(thermal_block_solver), ThermalOptions{}, + thermal_fields, couplingFields(solid_fields)); + + auto internal_variable_system = + buildInternalVariableSystem(std::make_shared(internal_variable_block_solver), + internal_variable_fields, couplingFields(solid_fields)); + + // Phase 3: register material integrands. + auto material = SimpleThermoelasticMaterial{}; + setCoupledThermoMechanicsInternalVariableMaterial(solid_system, thermal_system, internal_variable_system, material, + StrainDrivenInternalVariableMaterial{}, mesh_->entireBodyName()); + + // Phase 4: boundary conditions. + solid_system->setDisplacementBC(mesh_->domain("left")); + thermal_system->setTemperatureBC(mesh_->domain("left")); + + // Compressive traction on right face. + // Lambda args from addTraction: (t, X, n, u, v, a, temp_ss, temp_old, alpha_ss, alpha_old) + solid_system->addTraction( + "right", [](double, auto X, auto /*n*/, auto /*u*/, auto /*v*/, auto /*a*/, auto /*temp_ss*/, auto /*temp_old*/, + auto /*alpha_ss*/, auto /*alpha_old*/, auto... /*args*/) { + auto t = 0.0 * X; + t[0] = -0.005; + return t; + }); + + // Phase 5: combine and solve. + auto coupled_system = combineSystems(solid_system, thermal_system, internal_variable_system); + + double dt = 1.0, time = 0.0; + auto shape_disp = field_store->getShapeDisp(); + auto states = field_store->getStateFields(); + auto params = field_store->getParameterFields(); + std::vector reactions; + + for (size_t step = 0; step < 2; ++step) { + std::tie(states, reactions) = + makeAdvancer(coupled_system)->advanceState(smith::TimeInfo(time, dt, step), shape_disp, states, params); + time += dt; + } + + // All subsystems should converge and produce non-trivial solutions. + EXPECT_TRUE(solid_block_solver->nonlinear_solver_->nonlinearSolver().GetConverged()) + << "Solid solver did not converge"; + EXPECT_TRUE(thermal_block_solver->nonlinear_solver_->nonlinearSolver().GetConverged()) + << "Thermal solver did not converge"; + EXPECT_TRUE(internal_variable_block_solver->nonlinear_solver_->nonlinearSolver().GetConverged()) + << "Internal-variable solver did not converge"; + + // Displacement should be non-zero (compressive traction). + auto final_disp = states[field_store->getFieldIndex("displacement")].get(); + EXPECT_GT(final_disp->Normlinf(), 1e-8) << "Displacement should be non-zero under compressive traction"; + + // Temperature should be non-zero (heat source applied). + auto final_temp = states[field_store->getFieldIndex("temperature")].get(); + EXPECT_GT(final_temp->Normlinf(), 1e-10) << "Temperature should be non-zero under heat source"; + + // Damage should be non-zero (driven by strain norm). + auto final_state = states[field_store->getFieldIndex("state")].get(); + EXPECT_GT(final_state->Normlinf(), 1e-10) << "Damage state should grow under deformation"; +} + +} // namespace smith + +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + smith::ApplicationManager applicationManager(argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/smith/differentiable_numerics/thermal_system.hpp b/src/smith/differentiable_numerics/thermal_system.hpp index 126491a676..1a7f54baee 100644 --- a/src/smith/differentiable_numerics/thermal_system.hpp +++ b/src/smith/differentiable_numerics/thermal_system.hpp @@ -16,10 +16,11 @@ #include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" #include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" #include "smith/differentiable_numerics/time_integration_rule.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" #include "smith/differentiable_numerics/differentiable_physics.hpp" #include "smith/physics/weak_form.hpp" #include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/coupling_params.hpp" namespace smith { @@ -33,62 +34,30 @@ namespace smith { * @tparam dim Spatial dimension. * @tparam temp_order Order of the temperature basis. * @tparam TemperatureTimeRule Time integration rule type (must have num_states == 2). - * @tparam parameter_space Finite element spaces for optional parameters. + * @tparam Coupling Tuple of coupling and parameter packs (default: none). + * Coupling fields occupy leading positions in the tail after the 2 time-rule state fields, + * before user parameter_space fields. */ template + typename Coupling = std::tuple<>> struct ThermalSystem : public SystemBase { + using SystemBase::SystemBase; + static_assert(TemperatureTimeRule::num_states == 2, "ThermalSystem requires a 2-state time integration rule"); - /// @brief using for ThermalWeakFormType + /// Thermal weak form: (temp, temp_old, coupling_fields..., params...) using ThermalWeakFormType = - TimeDiscretizedWeakForm, - TimeRuleParams, parameter_space...>>; + FunctionalWeakForm, detail::TimeRuleParams, Coupling>>; std::shared_ptr thermal_weak_form; ///< Thermal weak form. std::shared_ptr temperature_bc; ///< Temperature boundary conditions. std::shared_ptr temperature_time_rule; ///< Time integration for temperature. - - /** - * @brief Get the list of all state fields (temperature_solve_state, temperature). - * @return std::vector List of state fields. - */ - std::vector getStateFields() const - { - return {field_store->getField(prefix("temperature_solve_state")), field_store->getField(prefix("temperature"))}; - } - - /** - * @brief Get the list of physical, non-solve state fields. - * @return std::vector List of physical fields suitable for output. - */ - std::vector getOutputFieldStates() const { return {field_store->getField(prefix("temperature"))}; } - - /** - * @brief Get information about reaction fields for this system. - * @return List of ReactionInfo structures. - */ - std::vector getReactionInfos() const - { - return {{prefix("thermal_flux"), &field_store->getField(prefix("temperature")).get()->space()}}; - } - - /** - * @brief Create a DifferentiablePhysics object for this system. - * @param physics_name The name of the physics. - * @return std::unique_ptr The differentiable physics object. - */ - std::unique_ptr createDifferentiablePhysics(std::string physics_name) - { - return std::make_unique(field_store->getMesh(), field_store->graph(), - field_store->getShapeDisp(), getStateFields(), getParameterFields(), - advancer, physics_name, getReactionInfos()); - } + std::shared_ptr coupling; ///< Coupling metadata for callback interpolation. /** * @brief Set the thermal material model for a domain. * - * Material is called as `material(x, temperature, grad_temperature, params...)` and must return + * Material is called as `material(t_info, temperature, grad_temperature, params...)` and must return * `smith::tuple{heat_capacity, heat_flux}`. Consistent with heat_transfer.hpp convention. * * The system forms the residual as: heat_capacity * dT/dt for the source term, and -heat_flux @@ -102,156 +71,272 @@ struct ThermalSystem : public SystemBase { void setMaterial(const MaterialType& material, const std::string& domain_name) { auto captured_temp_rule = temperature_time_rule; + auto captured_coupling = coupling; - thermal_weak_form->addBodyIntegral( - domain_name, [=](auto t_info, auto /*X*/, auto temperature, auto temperature_old, auto... params) { - auto [T_current, T_dot] = captured_temp_rule->interpolate(t_info, temperature, temperature_old); - auto [heat_capacity, heat_flux] = material(get(T_current), get(T_current), params...); - return smith::tuple{heat_capacity * get(T_dot), -heat_flux}; - }); + thermal_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_temp_rule, *captured_coupling, t_info, + [&](auto T_current, auto T_dot, auto... interpolated_params) { + auto [heat_capacity, heat_flux] = + material(t_info, get(T_current), get(T_current), interpolated_params...); + return smith::tuple{heat_capacity * get(T_dot), -heat_flux}; + }, + raw_args...); + }); } /** - * @brief Add a body heat source to the thermal system (with DependsOn). - * @param depends_on Selects which primal and parameter fields the contribution depends on. - * @param domain_name The name of the domain where the heat source is applied. - * @param source_function (t, X, T, params...) -> heat_source. + * @brief Set thermal material and a coincident body heat source from a single callable. + * + * The callable is invoked once per quadrature point and must return + * `smith::tuple{heat_capacity, heat_flux, heat_source}`. Used by coupled physics (e.g. + * thermo-mechanics) where one material evaluation produces all three contributions and we + * want to avoid re-evaluating the material for each piece. + * + * Residual contribution: `(heat_capacity * dT/dt - heat_source, -heat_flux)`. + * + * @tparam MaterialType The thermal material type. + * @param material The material model instance returning {C_v, q, s}. + * @param domain_name The name of the domain to apply the material to. */ - template - void addHeatSource(DependsOn depends_on, const std::string& domain_name, - HeatSourceType source_function) + template + void setMaterialAndHeatSource(const MaterialType& material, const std::string& domain_name) { auto captured_temp_rule = temperature_time_rule; + auto captured_coupling = coupling; - thermal_weak_form->addBodySource(depends_on, domain_name, - [=](auto t_info, auto X, auto temperature, auto temperature_old, auto... params) { - auto T = captured_temp_rule->value(t_info, temperature, temperature_old); - return source_function(t_info.time(), X, T, params...); - }); + thermal_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_temp_rule, *captured_coupling, t_info, + [&](auto T_current, auto T_dot, auto... interpolated_params) { + auto [heat_capacity, heat_flux, heat_source] = + material(t_info, get(T_current), get(T_current), interpolated_params...); + return smith::tuple{heat_capacity * get(T_dot) - heat_source, -heat_flux}; + }, + raw_args...); + }); } /** * @brief Add a body heat source that depends on all state and parameter fields. * @param domain_name The name of the domain where the heat source is applied. - * @param source_function (t, X, T, params...) -> heat_source. + * @param source_function (t_info, X, T, params...) -> heat_source. */ template void addHeatSource(const std::string& domain_name, HeatSourceType source_function) { - addHeatSourceAllParams(domain_name, source_function, std::make_index_sequence<2 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a boundary heat flux to the thermal system (with DependsOn). - * @param depends_on Selects which primal and parameter fields the contribution depends on. - * @param boundary_name The name of the boundary where the heat flux is applied. - * @param flux_function (t, X, n, T, params...) -> heat_flux. - */ - template - void addHeatFlux(DependsOn depends_on, const std::string& boundary_name, - HeatFluxType flux_function) - { - auto captured_temp_rule = temperature_time_rule; - - thermal_weak_form->addBoundaryFlux( - depends_on, boundary_name, - [=](auto t_info, auto X, auto n, auto temperature, auto temperature_old, auto... params) { - auto T = captured_temp_rule->value(t_info, temperature, temperature_old); - return -flux_function(t_info.time(), X, n, T, params...); - }); + auto captured_rule = temperature_time_rule; + auto captured_coupling = coupling; + thermal_weak_form->addBodySource(domain_name, [=](auto t_info, auto X, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto T_current, auto /*T_dot*/, auto... interpolated_params) { + return source_function(t_info.time(), X, T_current, interpolated_params...); + }, + raw_args...); + }); } /** * @brief Add a boundary heat flux that depends on all state and parameter fields. * @param boundary_name The name of the boundary where the heat flux is applied. - * @param flux_function (t, X, n, T, params...) -> heat_flux. + * @param flux_function (t_info, X, n, T, params...) -> heat_flux. */ template void addHeatFlux(const std::string& boundary_name, HeatFluxType flux_function) { - addHeatFluxAllParams(boundary_name, flux_function, std::make_index_sequence<2 + sizeof...(parameter_space)>{}); + auto captured_rule = temperature_time_rule; + auto captured_coupling = coupling; + thermal_weak_form->addBoundaryFlux(boundary_name, [=](auto t_info, auto X, auto n, auto... raw_args) { + return detail::applyTimeRuleAndCoupling( + *captured_rule, *captured_coupling, t_info, + [&](auto T_current, auto /*T_dot*/, auto... interpolated_params) { + return flux_function(t_info.time(), X, n, T_current, interpolated_params...); + }, + raw_args...); + }); } - private: - template - void addHeatSourceAllParams(const std::string& domain_name, HeatSourceType f, std::index_sequence) - { - addHeatSource(DependsOn(Is)...>{}, domain_name, f); - } + /// Set zero-temperature Dirichlet BC. + void setTemperatureBC(const Domain& domain) { temperature_bc->template setFixedScalarBCs(domain); } - template - void addHeatFluxAllParams(const std::string& boundary_name, HeatFluxType f, std::index_sequence) + /// Set temperature BC with a prescribed function. + template + void setTemperatureBC(const Domain& domain, AppliedTemperatureFunction f) { - addHeatFlux(DependsOn(Is)...>{}, boundary_name, f); + temperature_bc->template setScalarBCs(domain, f); } + + private: }; +struct ThermalOptions {}; + /** - * @brief Factory function to build a thermal system. - * @tparam dim Spatial dimension. - * @tparam temp_order Order of the temperature basis. - * @tparam TemperatureTimeRule Time integration rule type (must have num_states == 2). - * @tparam parameter_space Finite element spaces for optional parameters. - * @param mesh The mesh. - * @param solver The coupled system solver. - * @param temp_rule The time integration rule for temperature. - * @param prepend_name The name of the physics (used as field prefix). - * @param parameter_types Parameter field types. - * @return ThermalSystem with all components initialized. + * @brief Register all thermal fields into a FieldStore. + * + * Phase 1 of the two-phase initialization. + * + * @return PhysicsFields carrying the exported field tokens and time rule type. */ -template -ThermalSystem buildThermalSystem( - std::shared_ptr mesh, std::shared_ptr solver, TemperatureTimeRule temp_rule, - std::string prepend_name = "", FieldType... parameter_types) +template +auto registerThermalFields(std::shared_ptr field_store, + const ThermalOptions& /*options*/ = ThermalOptions{}) { - auto field_store = std::make_shared(mesh, 100); - - auto prefix = [&](const std::string& name) { - if (prepend_name.empty()) { - return name; - } - return prepend_name + "_" + name; - }; - - FieldType> shape_disp_type(prefix("shape_displacement")); - field_store->addShapeDisp(shape_disp_type); - - auto temperature_time_rule = std::make_shared(temp_rule); - FieldType> temperature_type(prefix("temperature_solve_state")); - auto temperature_bc = field_store->addIndependent(temperature_type, temperature_time_rule); - auto temperature_old_type = - field_store->addDependent(temperature_type, FieldStore::TimeDerivative::VAL, prefix("temperature")); - - std::vector parameter_fields; - (field_store->addParameter(FieldType(prefix("param_" + parameter_types.name))), ...); - (parameter_fields.push_back(field_store->getField(prefix("param_" + parameter_types.name))), ...); - - std::string thermal_flux_name = prefix("thermal_flux"); - auto thermal_weak_form = std::make_shared< - typename ThermalSystem::ThermalWeakFormType>( - thermal_flux_name, field_store->getMesh(), field_store->getField(temperature_type.name).get()->space(), - field_store->createSpaces(thermal_flux_name, temperature_type.name, temperature_type, temperature_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); - - std::vector> weak_forms{thermal_weak_form}; - auto advancer = std::make_shared(field_store, weak_forms, solver); - - return ThermalSystem{ - {field_store, solver, advancer, parameter_fields, prepend_name}, - thermal_weak_form, - temperature_bc, - temperature_time_rule}; + FieldType> shape_disp_type("shape_displacement"); + if (!field_store->hasField(field_store->prefix(shape_disp_type.name))) { + field_store->addShapeDisp(shape_disp_type); + } + + auto temperature_time_rule_ptr = std::make_shared(); + FieldType> temperature_type("temperature_solve_state"); + field_store->addIndependent(temperature_type, temperature_time_rule_ptr); + field_store->addDependent(temperature_type, FieldStore::TimeDerivative::VAL, "temperature"); + + return PhysicsFields, H1>{ + field_store, FieldType>(field_store->prefix("temperature_solve_state")), + FieldType>(field_store->prefix("temperature"))}; +} + +/** + * @brief Internal thermal builder after coupling fields are assembled. + * + * Phase 2 of the two-phase initialization. + */ +namespace detail { + +/// @brief Internal thermal builder after coupling fields are assembled. +template + requires detail::is_coupling_packs_v +auto buildThermalSystemImpl(std::shared_ptr field_store, const Coupling& coupling, + std::shared_ptr solver, const ThermalOptions& /*options*/) +{ + auto temperature_time_rule_ptr = std::make_shared(); + + FieldType> shape_disp_type(field_store->prefix("shape_displacement")); + FieldType> temperature_type(field_store->prefix("temperature_solve_state"), true); + FieldType> temperature_old_type(field_store->prefix("temperature")); + + auto temperature_bc = field_store->getBoundaryConditions(temperature_type.name); + + using SystemType = ThermalSystem; + + std::string thermal_flux_name = field_store->prefix("thermal_flux"); + auto thermal_weak_form = detail::buildWeakFormWithCoupling( + field_store, thermal_flux_name, temperature_type.name, temperature_type, temperature_old_type, + detail::flattenCouplingFields(coupling)); + + auto sys = + std::make_shared(field_store, solver, std::vector>{thermal_weak_form}); + sys->temperature_bc = temperature_bc; + sys->temperature_time_rule = temperature_time_rule_ptr; + sys->coupling = std::make_shared(coupling); + sys->thermal_weak_form = thermal_weak_form; + + return sys; +} + +} // namespace detail + +/** + * @brief Build a ThermalSystem from already-registered field packs. + * + * Deduces the temperature time rule from `self_fields`. + * + * Usage: + * @code + * auto thermal_system = buildThermalSystem( + * solver, opts, thermal_fields, couplingFields(solid_fields)); + * @endcode + */ +template + requires(detail::has_time_rule_v) +auto buildThermalSystem(std::shared_ptr solver, const ThermalOptions& options, + const SelfFields& self_fields) +{ + constexpr int dim = SelfFields::dim; + constexpr int temp_order = SelfFields::order; + using TemperatureTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(); + return detail::buildThermalSystemImpl(field_store, coupling, solver, options); +} + +/** + * @brief Build a ThermalSystem from registered self fields plus coupled physics fields. + */ +template + requires(detail::has_time_rule_v) +auto buildThermalSystem(std::shared_ptr solver, const ThermalOptions& options, + const SelfFields& self_fields, const CouplingFields& coupled) +{ + constexpr int dim = SelfFields::dim; + constexpr int temp_order = SelfFields::order; + using TemperatureTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled); + return detail::buildThermalSystemImpl(field_store, coupling, solver, options); +} + +/** + * @brief Build a ThermalSystem from registered self fields plus registered parameter fields. + */ +template + requires(detail::has_time_rule_v) +auto buildThermalSystem(std::shared_ptr solver, const ThermalOptions& options, + const SelfFields& self_fields, const ParamFields& params) +{ + constexpr int dim = SelfFields::dim; + constexpr int temp_order = SelfFields::order; + using TemperatureTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(params); + return detail::buildThermalSystemImpl(field_store, coupling, solver, options); } /** - * @brief Factory function to build a thermal system with default quasi-static rule (backward compatible). + * @brief Build a ThermalSystem from registered self fields, coupled physics fields, and parameter fields. */ -template -auto buildThermalSystem(std::shared_ptr mesh, std::shared_ptr solver, - std::string prepend_name = "", FieldType... parameter_types) +template + requires(detail::has_time_rule_v) +auto buildThermalSystem(std::shared_ptr solver, const ThermalOptions& options, + const SelfFields& self_fields, const CouplingFields& coupled, + const ParamFields& params) { - return buildThermalSystem(mesh, solver, QuasiStaticFirstOrderTimeIntegrationRule{}, prepend_name, - parameter_types...); + constexpr int dim = SelfFields::dim; + constexpr int temp_order = SelfFields::order; + using TemperatureTimeRule = typename std::decay_t::time_rule_type; + auto field_store = self_fields.field_store; + auto coupling = detail::collectCouplingFields(coupled, params); + return detail::buildThermalSystemImpl(field_store, coupling, solver, options); +} + +/** + * @brief Build a ThermalSystem from solver options and a mesh, registering parameter fields inline. + * + * Constructs the FieldStore, nonlinear block solver, system solver, and registers thermal + * and parameter fields, then delegates to the existing field-pack overload. + * + * @tparam dim Spatial dimension. + * @tparam temp_order Polynomial order for temperature. + * @tparam TemperatureTimeRule Time integration rule (defaults to QuasiStaticFirstOrderTimeIntegrationRule). + * @tparam ParamSpaces Parameter function-space tags deduced from FieldType arguments. + */ +template +auto buildThermalSystem(const NonlinearSolverOptions& nonlinear_opts, const LinearSolverOptions& linear_opts, + const ThermalOptions& options, std::shared_ptr mesh, + FieldType... params) +{ + auto field_store = std::make_shared(mesh); + auto solver = std::make_shared(buildNonlinearBlockSolver(nonlinear_opts, linear_opts, *mesh)); + auto thermal_fields = registerThermalFields(field_store, options); + if constexpr (sizeof...(ParamSpaces) > 0) { + auto param_fields = registerParameterFields(field_store, std::move(params)...); + return buildThermalSystem(solver, options, thermal_fields, param_fields); + } else { + return buildThermalSystem(solver, options, thermal_fields); + } } } // namespace smith diff --git a/src/smith/differentiable_numerics/thermo_mechanics_system.hpp b/src/smith/differentiable_numerics/thermo_mechanics_system.hpp index 49b83f8ffd..a398a0c751 100644 --- a/src/smith/differentiable_numerics/thermo_mechanics_system.hpp +++ b/src/smith/differentiable_numerics/thermo_mechanics_system.hpp @@ -6,495 +6,101 @@ /** * @file thermo_mechanics_system.hpp - * @brief Defines the ThermoMechanicsSystem struct and its factory function + * @brief Helper to wire a coupled thermo-mechanical material to a SolidMechanicsSystem and ThermalSystem. */ #pragma once -#include "smith/differentiable_numerics/field_store.hpp" -#include "smith/differentiable_numerics/nonlinear_block_solver.hpp" -#include "smith/differentiable_numerics/dirichlet_boundary_conditions.hpp" -#include "smith/differentiable_numerics/multiphysics_time_integrator.hpp" -#include "smith/differentiable_numerics/time_integration_rule.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" -#include "smith/differentiable_numerics/differentiable_physics.hpp" -#include "smith/physics/weak_form.hpp" -#include "smith/differentiable_numerics/system_base.hpp" +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" namespace smith { -/** - * @brief Container for a coupled thermo-mechanical system with configurable time integration. - * - * Displacement uses a 4-state second-order layout (displacement_solve_state, displacement, velocity, acceleration). - * Temperature uses a 2-state first-order layout (temperature_solve_state, temperature). - * Total: 6 state fields. - * - * @tparam dim Spatial dimension. - * @tparam disp_order Order of the displacement basis. - * @tparam temp_order Order of the temperature basis. - * @tparam DisplacementTimeRule Time integration rule type for displacement (must have num_states == 4). - * @tparam TemperatureTimeRule Time integration rule type for temperature (must have num_states == 2). - * @tparam parameter_space Finite element spaces for optional parameters. - */ -template -struct ThermoMechanicsSystem : public SystemBase { - static_assert(DisplacementTimeRule::num_states == 4, - "ThermoMechanicsSystem requires a 4-state displacement time rule"); - static_assert(TemperatureTimeRule::num_states == 2, "ThermoMechanicsSystem requires a 2-state temperature time rule"); - - /// @brief using for SolidWeakFormType - using SolidWeakFormType = - TimeDiscretizedWeakForm, - Parameters, H1, H1, - H1, H1, H1, parameter_space...>>; - - /// @brief using for ThermalWeakFormType - using ThermalWeakFormType = - TimeDiscretizedWeakForm, - Parameters, H1, H1, H1, - H1, H1, parameter_space...>>; - - // Cycle-zero weak form: test field = acceleration, inputs: u, v, a, temp, temp_old, params... - /// @brief using for CycleZeroWeakFormType - using CycleZeroWeakFormType = - TimeDiscretizedWeakForm, - Parameters, H1, H1, H1, - H1, parameter_space...>>; - - std::shared_ptr solid_weak_form; ///< Solid mechanics weak form. - std::shared_ptr thermal_weak_form; ///< Thermal weak form. - std::shared_ptr cycle_zero_weak_form; ///< Cycle-zero weak form. - std::shared_ptr disp_bc; ///< Displacement boundary conditions. - std::shared_ptr temperature_bc; ///< Temperature boundary conditions. - std::shared_ptr disp_time_rule; ///< Time integration for displacement. - std::shared_ptr temperature_time_rule; ///< Time integration for temperature. - - /** - * @brief Get the list of all state fields (disp_pred, disp, vel, accel, temp_pred, temp). - * @return std::vector List of state fields. - */ - std::vector getStateFields() const - { - return {field_store->getField(prefix("displacement_solve_state")), - field_store->getField(prefix("displacement")), - field_store->getField(prefix("velocity")), - field_store->getField(prefix("acceleration")), - field_store->getField(prefix("temperature_solve_state")), - field_store->getField(prefix("temperature"))}; - } - - /** - * @brief Get information about reaction fields for this system. - * @return List of ReactionInfo structures. - */ - std::vector getReactionInfos() const - { - return {{prefix("solid_force"), &field_store->getField(prefix("displacement")).get()->space()}, - {prefix("thermal_flux"), &field_store->getField(prefix("temperature")).get()->space()}}; - } - - /** - * @brief Create a DifferentiablePhysics object for this system. - * @param physics_name The name of the physics. - * @return std::unique_ptr The differentiable physics object. - */ - std::unique_ptr createDifferentiablePhysics(std::string physics_name) - { - return std::make_unique(field_store->getMesh(), field_store->graph(), - field_store->getShapeDisp(), getStateFields(), getParameterFields(), - advancer, physics_name, getReactionInfos()); - } - - /** - * @brief Set the material model for a domain, defining integrals for solid and thermal weak forms. - * - * The material is called as material(dt, state, grad_u, grad_v, T, grad_T, params...) and - * must expose a `density` member for the cycle-zero acceleration solve. - * - * @tparam MaterialType The material model type. - * @param material The material model instance. - * @param domain_name The name of the domain to apply the material to. - */ - template - void setMaterial(const MaterialType& material, const std::string& domain_name) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - solid_weak_form->addBodyIntegral( - domain_name, [=](auto t_info, auto /*X*/, auto u, auto u_old, auto v_old, auto a_old, auto temperature, - auto temperature_old, auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto T = captured_temp_rule->value(t_info, temperature, temperature_old); - - typename MaterialType::State state; - auto [pk, C_v, s0, q0] = material(t_info.dt(), state, get(u_current), get(v_current), - get(T), get(T), params...); - return smith::tuple{get(a_current) * material.density, pk}; - }); - - thermal_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto T, auto T_old, auto disp, - auto disp_old, auto v_old, auto a_old, auto... params) { - auto [T_current, T_dot] = captured_temp_rule->interpolate(t_info, T, T_old); - auto [u, v, a] = captured_disp_rule->interpolate(t_info, disp, disp_old, v_old, a_old); - - typename MaterialType::State state; - auto [pk, C_v, s0, q0] = material(t_info.dt(), state, get(u), get(v), - get(T_current), get(T_current), params...); - auto dT_dt = get(T_dot); - return smith::tuple{C_v * dT_dt - s0, -q0}; - }); - - // Cycle-zero: u and v are given, solve for a; temperature at initial condition - cycle_zero_weak_form->addBodyIntegral(domain_name, [=](auto t_info, auto /*X*/, auto u, auto v, auto a, - auto temperature, auto temperature_old, auto... params) { - auto T = captured_temp_rule->value(t_info, temperature, temperature_old); - - typename MaterialType::State state; - auto [pk, C_v, s0, q0] = material(t_info.dt(), state, get(u), get(v), get(T), - get(T), params...); - return smith::tuple{get(a) * material.density, pk}; - }); - } - - /** - * @brief Add a body force to the solid mechanics part of the system (with DependsOn). - */ - template - void addSolidBodyForce(DependsOn depends_on, const std::string& domain_name, - BodyForceType force_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - solid_weak_form->addBodySource( - depends_on, domain_name, - [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto temperature, auto temperature_old, - auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [current_T, T_dot] = captured_temp_rule->interpolate(t_info, temperature, temperature_old); - return force_function(t_info.time(), X, u_current, v_current, a_current, current_T, T_dot, params...); - }); - } - - /** - * @brief Add a body force to the solid mechanics part of the system. - */ - template - void addSolidBodyForce(const std::string& domain_name, BodyForceType force_function) - { - addSolidBodyForceAllParams(domain_name, force_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a surface traction to the solid mechanics part (with DependsOn). - */ - template - void addSolidTraction(DependsOn depends_on, const std::string& domain_name, - SurfaceFluxType flux_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - solid_weak_form->addBoundaryFlux( - depends_on, domain_name, - [=](auto t_info, auto X, auto n, auto u, auto u_old, auto v_old, auto a_old, auto temperature, - auto temperature_old, auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [current_T, T_dot] = captured_temp_rule->interpolate(t_info, temperature, temperature_old); - return flux_function(t_info.time(), X, n, u_current, v_current, a_current, current_T, T_dot, params...); - }); - } - - /** - * @brief Add a surface traction to the solid mechanics part. - */ - template - void addSolidTraction(const std::string& domain_name, SurfaceFluxType flux_function) - { - addSolidTractionAllParams(domain_name, flux_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}, - std::make_index_sequence<5 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a body heat source to the thermal part (with DependsOn). - */ - template - void addHeatSource(DependsOn depends_on, const std::string& domain_name, - BodySourceType source_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - thermal_weak_form->addBodySource( - depends_on, domain_name, - [=](auto t_info, auto X, auto T, auto T_old, auto disp, auto disp_old, auto v_old, auto a_old, auto... params) { - auto [current_u, v_current, a_current] = - captured_disp_rule->interpolate(t_info, disp, disp_old, v_old, a_old); - auto [T_current, T_dot] = captured_temp_rule->interpolate(t_info, T, T_old); - return source_function(t_info.time(), X, current_u, v_current, a_current, T_current, T_dot, params...); - }); - } - - /** - * @brief Add a body heat source to the thermal part. - */ - template - void addHeatSource(const std::string& domain_name, BodySourceType source_function) - { - addHeatSourceAllParams(domain_name, source_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a boundary heat flux to the thermal part (with DependsOn). - */ - template - void addHeatFlux(DependsOn depends_on, const std::string& domain_name, - SurfaceFluxType flux_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - thermal_weak_form->addBoundaryFlux(depends_on, domain_name, - [=](auto t_info, auto X, auto n, auto T, auto T_old, auto disp, auto disp_old, - auto v_old, auto a_old, auto... params) { - auto [current_u, v_current, a_current] = - captured_disp_rule->interpolate(t_info, disp, disp_old, v_old, a_old); - auto [T_current, T_dot] = captured_temp_rule->interpolate(t_info, T, T_old); - return -flux_function(t_info.time(), X, n, current_u, v_current, a_current, - T_current, T_dot, params...); - }); - } - - /** - * @brief Add a boundary heat flux to the thermal part. - */ - template - void addHeatFlux(const std::string& domain_name, SurfaceFluxType flux_function) - { - addHeatFluxAllParams(domain_name, flux_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - /** - * @brief Add a pressure boundary condition (follower force) to the solid part (with DependsOn). - */ - template - void addPressure(DependsOn depends_on, const std::string& domain_name, - PressureType pressure_function) - { - auto captured_disp_rule = disp_time_rule; - auto captured_temp_rule = temperature_time_rule; - - solid_weak_form->addBoundaryIntegral( - depends_on, domain_name, - [=](auto t_info, auto X, auto u, auto u_old, auto v_old, auto a_old, auto temperature, auto temperature_old, - auto... params) { - auto [u_current, v_current, a_current] = captured_disp_rule->interpolate(t_info, u, u_old, v_old, a_old); - auto [T_current, T_dot] = captured_temp_rule->interpolate(t_info, temperature, temperature_old); - - auto x_current = X + u_current; - auto n_deformed = cross(get(x_current)); - auto n_shape_norm = norm(cross(get(X))); - - auto pressure = pressure_function(t_info.time(), get(X), u_current, v_current, a_current, T_current, - T_dot, get(params)...); - - return pressure * n_deformed * (1.0 / n_shape_norm); - }); - } - - /** - * @brief Add a pressure boundary condition (follower force) to the solid part. - */ - template - void addPressure(const std::string& domain_name, PressureType pressure_function) - { - addPressureAllParams(domain_name, pressure_function, std::make_index_sequence<6 + sizeof...(parameter_space)>{}); - } - - private: - template - void addSolidBodyForceAllParams(const std::string& domain_name, BodyForceType force_function, - std::index_sequence) - { - addSolidBodyForce(DependsOn(Is)...>{}, domain_name, force_function); - } - - template - void addSolidTractionAllParams(const std::string& domain_name, SurfaceFluxType flux_function, - std::index_sequence, std::index_sequence) - { - addSolidTraction(DependsOn(MainIs)...>{}, domain_name, flux_function); - - auto captured_temp_rule = temperature_time_rule; - cycle_zero_weak_form->addBoundaryFlux( - DependsOn(CycleZeroIs)...>{}, domain_name, - [=](auto t_info, auto X, auto n, auto u, auto v, auto a, auto temperature, auto temperature_old, - auto... params) { - auto [current_T, T_dot] = captured_temp_rule->interpolate(t_info, temperature, temperature_old); - return flux_function(t_info.time(), X, n, u, v, a, current_T, T_dot, params...); - }); - } - - template - void addPressureAllParams(const std::string& domain_name, PressureType pressure_function, std::index_sequence) - { - addPressure(DependsOn(Is)...>{}, domain_name, pressure_function); - } - - template - void addHeatSourceAllParams(const std::string& domain_name, BodySourceType source_function, - std::index_sequence) - { - addHeatSource(DependsOn(Is)...>{}, domain_name, source_function); - } - - template - void addHeatFluxAllParams(const std::string& domain_name, SurfaceFluxType flux_function, std::index_sequence) - { - addHeatFlux(DependsOn(Is)...>{}, domain_name, flux_function); - } -}; +namespace detail { /** - * @brief Factory function to build a thermo-mechanical system. - * @param mesh The mesh. - * @param solver The coupled system solver. - * @param disp_rule The displacement time integration rule. - * @param temp_rule The temperature time integration rule. - * @param prepend_name Optional field-name prefix. - * @param cycle_zero_solver Optional override for the cycle-zero solve. Defaults to - * `solver->singleBlockSolver(0)`. - * @param parameter_types Optional parameter field descriptors. + * @brief Evaluate coupled thermo-mechanical material using TimeInfo-aware signature. */ -template -ThermoMechanicsSystem -buildThermoMechanicsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, TemperatureTimeRule temp_rule, std::string prepend_name = "", - std::shared_ptr cycle_zero_solver = nullptr, - FieldType... parameter_types) +template +auto evaluateCoupledThermoMechanicsMaterial(const MaterialType& material, const TimeInfo& t_info, StateType& state, + const GradUType& grad_u, const GradVType& grad_v, ThetaType theta, + const GradThetaType& grad_theta, ParamTypes&&... params) { - auto field_store = std::make_shared(mesh, 100); - - auto prefix = [&](const std::string& name) { - if (prepend_name.empty()) { - return name; - } - return prepend_name + "_" + name; - }; - - FieldType> shape_disp_type(prefix("shape_displacement")); - field_store->addShapeDisp(shape_disp_type); - - // Displacement fields (4-state second-order) - auto disp_time_rule_ptr = std::make_shared(disp_rule); - FieldType> disp_type(prefix("displacement_solve_state")); - auto disp_bc = field_store->addIndependent(disp_type, disp_time_rule_ptr); - auto disp_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::VAL, prefix("displacement")); - auto velo_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DOT, prefix("velocity")); - auto accel_old_type = field_store->addDependent(disp_type, FieldStore::TimeDerivative::DDOT, prefix("acceleration")); - - // Temperature fields (2-state first-order) - auto temperature_time_rule_ptr = std::make_shared(temp_rule); - FieldType> temperature_type(prefix("temperature_solve_state")); - auto temperature_bc = field_store->addIndependent(temperature_type, temperature_time_rule_ptr); - auto temperature_old_type = - field_store->addDependent(temperature_type, FieldStore::TimeDerivative::VAL, prefix("temperature")); - - std::vector parameter_fields; - (field_store->addParameter(FieldType(prefix("param_" + parameter_types.name))), ...); - (parameter_fields.push_back(field_store->getField(prefix("param_" + parameter_types.name))), ...); - - using SystemType = - ThermoMechanicsSystem; - - // Solid mechanics weak form (u, u_old, v_old, a_old, temp, temp_old, params...) - std::string solid_force_name = prefix("solid_force"); - auto solid_weak_form = std::make_shared( - solid_force_name, field_store->getMesh(), field_store->getField(disp_type.name).get()->space(), - field_store->createSpaces(solid_force_name, disp_type.name, disp_type, disp_old_type, velo_old_type, - accel_old_type, temperature_type, temperature_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); + return material(t_info, state, grad_u, grad_v, theta, grad_theta, std::forward(params)...); +} - // Thermal weak form (temp, temp_old, u, u_old, v_old, a_old, params...) - std::string thermal_flux_name = prefix("thermal_flux"); - auto thermal_weak_form = std::make_shared( - thermal_flux_name, field_store->getMesh(), field_store->getField(temperature_type.name).get()->space(), - field_store->createSpaces(thermal_flux_name, temperature_type.name, temperature_type, temperature_old_type, - disp_type, disp_old_type, velo_old_type, accel_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); +template +/// @brief Adapts coupled thermo-mechanical material to solid-system material interface. +struct CoupledSolidThermoMechanicsMaterialAdapter { + /// Material state type forwarded to solid system. + using State = typename MaterialType::State; - // Cycle-zero weak form (u, v, a, temp, temp_old, params...) - std::string cycle_zero_name = prefix("solid_reaction"); - auto cycle_zero_weak_form = std::make_shared( - cycle_zero_name, field_store->getMesh(), field_store->getField(accel_old_type.name).get()->space(), - field_store->createSpaces(cycle_zero_name, accel_old_type.name, disp_type, velo_old_type, accel_old_type, - temperature_type, temperature_old_type, - FieldType(prefix("param_" + parameter_types.name))...)); + MaterialType material; ///< Wrapped thermo-mechanical material. + double density; ///< Material density exposed for solid residual. - if (cycle_zero_solver == nullptr) { - cycle_zero_solver = solver->singleBlockSolver(0); + template + /// @brief Evaluate wrapped material and return solid PK1 contribution. + auto operator()(const TimeInfo& t_info, StateType& state, GradUType grad_u, GradVType grad_v, + TemperatureType temperature, TemperatureDotType /*temperature_dot*/, ParamTypes&&... params) const + { + auto [pk, C_v, s0, q0] = + evaluateCoupledThermoMechanicsMaterial(material, t_info, state, grad_u, grad_v, get(temperature), + get(temperature), std::forward(params)...); + return pk; } - SLIC_ERROR_IF(cycle_zero_solver == nullptr, - "Could not derive a cycle-zero solver for block 0 from the provided thermo-mechanics solver."); - - // Build solver and advancer - std::vector> weak_forms{solid_weak_form, thermal_weak_form}; - auto advancer = std::make_shared(field_store, weak_forms, solver, cycle_zero_weak_form, - cycle_zero_solver); - - return SystemType{{field_store, solver, advancer, parameter_fields, prepend_name}, - solid_weak_form, - thermal_weak_form, - cycle_zero_weak_form, - disp_bc, - temperature_bc, - disp_time_rule_ptr, - temperature_time_rule_ptr}; -} +}; -/** - * @brief Factory function to build a thermo-mechanical system with a physics name and parameter fields. - */ -template -auto buildThermoMechanicsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, TemperatureTimeRule temp_rule, std::string prepend_name, - FieldType... parameter_types) -{ - return buildThermoMechanicsSystem(mesh, solver, disp_rule, temp_rule, - std::move(prepend_name), nullptr, parameter_types...); -} +} // namespace detail /** - * @brief Factory function to build a thermo-mechanical system (without physics name). - */ -template -auto buildThermoMechanicsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, TemperatureTimeRule temp_rule, - std::shared_ptr cycle_zero_solver, - FieldType... parameter_types) -{ - return buildThermoMechanicsSystem(mesh, solver, disp_rule, temp_rule, "", - cycle_zero_solver, parameter_types...); -} - -/** - * @brief Factory function to build a thermo-mechanical system (without physics name). + * @brief Register a coupled thermo-mechanical material integrand on a SolidMechanicsSystem + * and ThermalSystem that were built with mutual coupling fields. + * + * Assumes: + * - solid was built with thermal fields as the leading coupling fields + * (first 2 coupling positions: temperature_solve_state, temperature). + * - thermal was built with solid displacement fields as the leading coupling fields + * (first 4 coupling positions: displacement_solve_state, displacement, velocity, acceleration). + * + * The material callable must satisfy: + * material(t_info, state, grad_u, grad_v, T_value, grad_T, params...) -> tuple{PK1, C_v, s0, q0} + * + * @tparam dim Spatial dimension (deduced from SolidMechanicsSystem template arg). + * @tparam disp_order_ Displacement polynomial order. + * @tparam temp_order_ Temperature polynomial order. + * @tparam DispRule Displacement time integration rule. + * @tparam TempRule Temperature time integration rule. + * @tparam SolidCoupling Coupling type on the solid system. + * @tparam ThermalCoupling Coupling type on the thermal system. + * @tparam MaterialType Material model type. + * @param solid Solid mechanics system with thermal coupling. + * @param thermal Thermal system with solid displacement coupling. + * @param material Material model instance. + * @param domain_name Domain on which to apply the material. */ -template -auto buildThermoMechanicsSystem(std::shared_ptr mesh, std::shared_ptr solver, - DisplacementTimeRule disp_rule, TemperatureTimeRule temp_rule, - FieldType... parameter_types) +template +void setCoupledThermoMechanicsMaterial( + std::shared_ptr> solid, + std::shared_ptr> thermal, const MaterialType& material, + const std::string& domain_name) { - return buildThermoMechanicsSystem(mesh, solver, disp_rule, temp_rule, "", nullptr, - parameter_types...); + auto solid_material = detail::CoupledSolidThermoMechanicsMaterialAdapter{material, material.density}; + + solid->setMaterial(solid_material, domain_name); + + thermal->setMaterialAndHeatSource( + [=](const TimeInfo& t_info, auto temperature, auto grad_temperature, auto u, auto v, auto /*a*/, auto... params) { + typename MaterialType::State state{}; + auto [pk, C_v, s0, q0] = detail::evaluateCoupledThermoMechanicsMaterial( + material, t_info, state, get(u), get(v), temperature, grad_temperature, params...); + // Material's s0 sits on the LHS (residual = C_v*T_dot + s0 - q·∇v); negate to fit the + // physical-heat-source convention used by setMaterialAndHeatSource. + return smith::tuple{C_v, q0, -s0}; + }, + domain_name); } } // namespace smith diff --git a/src/smith/differentiable_numerics/thermo_mechanics_with_internal_vars_system.hpp b/src/smith/differentiable_numerics/thermo_mechanics_with_internal_vars_system.hpp new file mode 100644 index 0000000000..0f3a9505cb --- /dev/null +++ b/src/smith/differentiable_numerics/thermo_mechanics_with_internal_vars_system.hpp @@ -0,0 +1,62 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#pragma once + +#include "smith/differentiable_numerics/solid_mechanics_system.hpp" +#include "smith/differentiable_numerics/thermal_system.hpp" +#include "smith/differentiable_numerics/state_variable_system.hpp" +#include "smith/differentiable_numerics/solid_mechanics_with_internal_vars_system.hpp" +#include "smith/differentiable_numerics/thermo_mechanics_system.hpp" + +namespace smith { + +/** + * @brief Wire a coupled thermo-mechanical internal-variable material across solid, thermal, and internal-variable + * systems by composing the existing two-system helpers. + * + * Preconditions on the systems passed in: + * - @p solid_system was built with @c (thermal_fields, internal_variable_fields) coupling, in that order. The + * leading 4 tail arguments are then @c (T_solve_state, T_old, alpha_solve_state, alpha_old). The thermo-mech + * material must accept @c (t_info, state, grad_u, grad_v, theta, grad_theta, alpha, ...) — alpha is forwarded + * from the trailing variadic params. + * - @p thermal_system was built with @c (solid_fields, internal_variable_fields) coupling. The thermo-mech material + * must accept @c (t_info, state, grad_u, grad_v, theta, grad_theta, alpha, ...). + * - @p internal_variable_system was built with @c solid_fields coupling. The evolution callable must accept + * @c (t_info, alpha, alpha_dot, grad_u, ...). + * - The internal-variable time rule's @c value() must equal its current state (true for backward Euler), so the + * raw @c alpha solve-state may be forwarded into the thermo-mech material in place of an interpolated alpha. + * + * Routing through @c solid->setMaterial and @c thermal->setMaterialAndHeatSource means cycle-zero and + * stress-output paths automatically receive the coupled stress contribution. + * + * @tparam SolidSystem Type of the solid mechanics system. + * @tparam ThermalSystem Type of the thermal system. + * @tparam InternalVariableSystem Type of the internal variable system. + * @tparam ThermoMechMaterial Type of the thermo-mechanical material integrand. + * @tparam InternalVarEvolution Type of the internal variable evolution integrand. + * + * @param solid_system The solid mechanics system. + * @param thermal_system The thermal system. + * @param internal_variable_system The internal variable system. + * @param thermo_mech_material The material model for stress, heat capacity, heat source, and heat flux. + * @param internal_var_evolution The ODE residual for the internal variable. + * @param domain_name The domain name to apply the material to. + */ +template +void setCoupledThermoMechanicsInternalVariableMaterial(std::shared_ptr solid_system, + std::shared_ptr thermal_system, + std::shared_ptr internal_variable_system, + ThermoMechMaterial thermo_mech_material, + InternalVarEvolution internal_var_evolution, + const std::string& domain_name) +{ + setCoupledThermoMechanicsMaterial(solid_system, thermal_system, thermo_mech_material, domain_name); + setCoupledInternalVariableMaterial(internal_variable_system, solid_system, internal_var_evolution, domain_name); +} + +} // namespace smith diff --git a/src/smith/differentiable_numerics/time_discretized_weak_form.hpp b/src/smith/differentiable_numerics/time_discretized_weak_form.hpp deleted file mode 100644 index 1c4f06ab93..0000000000 --- a/src/smith/differentiable_numerics/time_discretized_weak_form.hpp +++ /dev/null @@ -1,356 +0,0 @@ -// Copyright (c) Lawrence Livermore National Security, LLC and -// other Smith Project Developers. See the top-level LICENSE file for -// details. -// -// SPDX-License-Identifier: (BSD-3-Clause) - -/** - * @file time_discretized_weak_form.hpp - * - * @brief Wraps FunctionalWeakForm to provide TimeInfo (time, dt, cycle) to integrands instead of just time. - * - * This class provides a thin wrapper around FunctionalWeakForm that automatically converts the time - * parameter into a TimeInfo struct containing time, timestep size (dt), and cycle number. This allows - * physics systems to access timestep information needed for time integration. - * - * Key features: - * - All integrands receive TimeInfo instead of just time - * - Systems are responsible for manually applying time integration rules - * - Supports body integrals, boundary integrals, boundary fluxes, and interior boundary integrals - * - Default behavior passes ALL input fields to integrands (can be overridden with DependsOn) - */ - -#pragma once - -#include "smith/physics/functional_weak_form.hpp" -#include "smith/physics/mesh.hpp" -#include "smith/differentiable_numerics/field_state.hpp" -#include "smith/differentiable_numerics/time_integration_rule.hpp" - -namespace smith { - -template > -class TimeDiscretizedWeakForm; - -/** - * @brief A time-discretized weak form that provides TimeInfo to integrands. - * - * This wraps FunctionalWeakForm to pass TimeInfo (containing time, dt, and cycle) to all - * integrand functions instead of just the time value. This allows physics models to access - * timestep information for time integration. - * - * @tparam spatial_dim The spatial dimension for the problem. - * @tparam OutputSpace The output residual for the weak form (test-space). - * @tparam InputSpaces All the input FiniteElementState fields (trial-spaces). - */ -template -class TimeDiscretizedWeakForm> - : public FunctionalWeakForm> { - public: - using Base = FunctionalWeakForm>; ///< Base class alias - - /** - * @brief Construct a time-discretized weak form. - * @param physics_name Unique name for this physics module. - * @param mesh The computational mesh. - * @param output_mfem_space The test function space (output/residual space). - * @param input_mfem_spaces Vector of trial function spaces (input field spaces). - */ - TimeDiscretizedWeakForm(std::string physics_name, std::shared_ptr mesh, - const mfem::ParFiniteElementSpace& output_mfem_space, - const typename Base::SpacesT& input_mfem_spaces) - : Base(physics_name, mesh, output_mfem_space, input_mfem_spaces) - { - } - - /** - * @brief Add a body integral with TimeInfo. - * - * The integrand receives TimeInfo (containing time, dt, cycle) instead of just time. - * The system is responsible for manually applying time integration rules inside the integrand - * to compute current state values from the raw field history. - * - * @tparam active_parameters Indices of fields this integral depends on. - * @tparam BodyIntegralType The integrand function type. - * @param depends_on Dependency specification for which input fields to pass. - * @param body_name The name of the domain. - * @param integrand Function with signature (TimeInfo, X, inputs...) -> residual. - */ - template - void addBodyIntegral(DependsOn depends_on, std::string body_name, BodyIntegralType integrand) - { - const double* dt = &this->dt_; - const size_t* cycle = &this->cycle_; - Base::addBodyIntegral(depends_on, body_name, [dt, cycle, integrand](double t, auto X, auto... inputs) { - TimeInfo time_info(t, *dt, *cycle); - return integrand(time_info, X, inputs...); - }); - } - - /** - * @brief Add a body integral with TimeInfo (defaults to all input fields). - * @tparam BodyIntegralType The integrand function type. - * @param body_name The name of the domain. - * @param integrand Function with signature (TimeInfo, X, inputs...) -> residual. - */ - template - void addBodyIntegral(std::string body_name, BodyIntegralType integrand) - { - constexpr int num_inputs = sizeof...(InputSpaces); - addBodyIntegralWithAllParams(body_name, integrand, std::make_integer_sequence{}); - } - - /** - * @brief Add a body source (body load) with TimeInfo. - * @tparam active_parameters Indices of fields this source depends on. - * @tparam BodyLoadType The load function type. - * @param depends_on Dependency specification. - * @param body_name The name of the domain. - * @param load_function Function with signature (TimeInfo, X, inputs...) -> load vector. - */ - template - void addBodySource(DependsOn depends_on, std::string body_name, BodyLoadType load_function) - { - addBodyIntegral(depends_on, body_name, [load_function](auto t_info, auto X, auto... inputs) { - return smith::tuple{-load_function(t_info, get(X), get(inputs)...), smith::zero{}}; - }); - } - - /// defaults to use all parameters - /// @overload - template - void addBodySource(std::string body_name, BodyLoadType load_function) - { - constexpr int num_inputs = sizeof...(InputSpaces); - addBodySourceWithAllParams(body_name, load_function, std::make_integer_sequence{}); - } - - /** - * @brief Add a boundary integral with TimeInfo. - * @tparam active_parameters Indices of fields this integral depends on. - * @tparam BoundaryIntegralType The boundary integrand function type. - * @param depends_on Dependency specification. - * @param boundary_name The name of the boundary. - * @param integrand Function with signature (TimeInfo, X, inputs...) -> residual. - */ - template - void addBoundaryIntegral(DependsOn depends_on, std::string boundary_name, - BoundaryIntegralType integrand) - { - const double* dt = &this->dt_; - const size_t* cycle = &this->cycle_; - Base::addBoundaryIntegral(depends_on, boundary_name, [dt, cycle, integrand](double t, auto X, auto... inputs) { - TimeInfo time_info(t, *dt, *cycle); - return integrand(time_info, X, inputs...); - }); - } - - /// defaults to use all parameters - /// @overload - template - void addBoundaryIntegral(std::string boundary_name, BoundaryIntegralType integrand) - { - constexpr int num_inputs = sizeof...(InputSpaces); - addBoundaryIntegralWithAllParams(boundary_name, integrand, std::make_integer_sequence{}); - } - - /** - * @brief Add a boundary flux with TimeInfo. - * @tparam active_parameters Indices of fields this integral depends on. - * @tparam BoundaryFluxType The flux function type. - * @param depends_on Dependency specification. - * @param boundary_name The name of the boundary. - * @param flux_function Function with signature (TimeInfo, X, n, inputs...) -> flux. - */ - template - void addBoundaryFlux(DependsOn depends_on, std::string boundary_name, - BoundaryFluxType flux_function) - { - const double* dt = &this->dt_; - const size_t* cycle = &this->cycle_; - Base::addBoundaryFlux(depends_on, boundary_name, - [dt, cycle, flux_function](double t, auto X, auto n, auto... inputs) { - TimeInfo time_info(t, *dt, *cycle); - return flux_function(time_info, X, n, inputs...); - }); - } - - /// defaults to use all parameters - /// @overload - template - void addBoundaryFlux(std::string boundary_name, BoundaryFluxType flux_function) - { - constexpr int num_inputs = sizeof...(InputSpaces); - addBoundaryFluxWithAllParams(boundary_name, flux_function, std::make_integer_sequence{}); - } - - /** - * @brief Add an interior boundary integral with TimeInfo. - * @tparam active_parameters Indices of fields this integral depends on. - * @tparam InteriorIntegralType The integrand function type. - * @param depends_on Dependency specification. - * @param interior_name The name of the interior boundary. - * @param integrand Function with signature (TimeInfo, X, inputs...) -> residual. - */ - template - void addInteriorBoundaryIntegral(DependsOn depends_on, std::string interior_name, - InteriorIntegralType integrand) - { - const double* dt = &this->dt_; - const size_t* cycle = &this->cycle_; - Base::addInteriorBoundaryIntegral(depends_on, interior_name, - [dt, cycle, integrand](double t, auto X, auto... inputs) { - TimeInfo time_info(t, *dt, *cycle); - return integrand(time_info, X, inputs...); - }); - } - - /// defaults to use all parameters - /// @overload - template - void addInteriorBoundaryIntegral(std::string interior_name, InteriorIntegralType integrand) - { - constexpr int num_inputs = sizeof...(InputSpaces); - addInteriorBoundaryIntegralWithAllParams(interior_name, integrand, std::make_integer_sequence{}); - } - - private: - template - void addBodyIntegralWithAllParams(std::string body_name, BodyIntegralType integrand, - std::integer_sequence) - { - addBodyIntegral(DependsOn{}, body_name, integrand); - } - - template - void addBodySourceWithAllParams(std::string body_name, BodyLoadType load_function, - std::integer_sequence) - { - addBodySource(DependsOn{}, body_name, load_function); - } - - template - void addBoundaryIntegralWithAllParams(std::string boundary_name, BoundaryIntegralType integrand, - std::integer_sequence) - { - addBoundaryIntegral(DependsOn{}, boundary_name, integrand); - } - - template - void addInteriorBoundaryIntegralWithAllParams(std::string interior_name, InteriorIntegralType integrand, - std::integer_sequence) - { - addInteriorBoundaryIntegral(DependsOn{}, interior_name, integrand); - } - - template - void addBoundaryFluxWithAllParams(std::string boundary_name, BoundaryFluxType flux_function, - std::integer_sequence) - { - addBoundaryFlux(DependsOn{}, boundary_name, flux_function); - } -}; - -/// @brief A container holding the weak forms useful for second-order time-discretized systems. -class SecondOrderTimeDiscretizedWeakForms { - public: - std::shared_ptr time_discretized_weak_form; ///< Weak form in terms of predicted/current unknown. - std::shared_ptr final_reaction_weak_form; ///< Weak form in terms of converged kinematic states. -}; - -template > -class SecondOrderTimeDiscretizedWeakForm; - -/// @brief Convenience wrapper for second-order-in-time systems using an implicit Newmark rule. -template -class SecondOrderTimeDiscretizedWeakForm> - : public SecondOrderTimeDiscretizedWeakForms { - public: - static constexpr int NUM_STATE_VARS = 4; ///< u, u_old, v_old, a_old - - /// @brief Predicted-state weak form type. - using TimeDiscretizedWeakFormT = - TimeDiscretizedWeakForm>; - /// @brief Final corrected-state reaction weak form type. - using FinalReactionFormT = TimeDiscretizedWeakForm>; - - /// @brief Construct paired weak forms for second-order systems. - SecondOrderTimeDiscretizedWeakForm(std::string physics_name, std::shared_ptr mesh, - ImplicitNewmarkSecondOrderTimeIntegrationRule time_rule, - const mfem::ParFiniteElementSpace& output_mfem_space, - const typename TimeDiscretizedWeakFormT::SpacesT& input_mfem_spaces) - : time_rule_(time_rule) - { - time_discretized_weak_form_ = - std::make_shared(physics_name, mesh, output_mfem_space, input_mfem_spaces); - time_discretized_weak_form = time_discretized_weak_form_; - - typename TimeDiscretizedWeakFormT::SpacesT trial_removed_spaces(std::next(input_mfem_spaces.begin()), - input_mfem_spaces.end()); - final_reaction_weak_form_ = - std::make_shared(physics_name, mesh, output_mfem_space, trial_removed_spaces); - final_reaction_weak_form = final_reaction_weak_form_; - } - - /// @brief Add a body integral using corrected second-order kinematics. - template - void addBodyIntegral(DependsOn /*depends_on*/, std::string body_name, - BodyIntegralType integrand) - { - auto time_rule = time_rule_; - time_discretized_weak_form_->addBodyIntegral( - DependsOn<0, 1, 2, 3, NUM_STATE_VARS + active_parameters...>{}, body_name, - [integrand, time_rule](const TimeInfo& t, auto X, auto U, auto U_old, auto U_dot_old, auto U_dot_dot_old, - auto... inputs) { - return integrand(t, X, time_rule.value(t, U, U_old, U_dot_old, U_dot_dot_old), - time_rule.dot(t, U, U_old, U_dot_old, U_dot_dot_old), - time_rule.ddot(t, U, U_old, U_dot_old, U_dot_dot_old), inputs...); - }); - final_reaction_weak_form_->addBodyIntegral(DependsOn<0, 1, 2, NUM_STATE_VARS - 1 + active_parameters...>{}, - body_name, integrand); - } - - /// @brief Add a body integral using all trailing inputs. - template - void addBodyIntegral(std::string body_name, BodyIntegralType integrand) - { - addBodyIntegral(DependsOn<>{}, body_name, integrand); - } - - /// @brief Add a body source using corrected second-order kinematics. - template - void addBodySource(DependsOn /*depends_on*/, std::string body_name, BodyLoadType load_function) - { - auto time_rule = time_rule_; - time_discretized_weak_form_->addBodyIntegral( - DependsOn<0, 1, 2, 3, NUM_STATE_VARS + active_parameters...>{}, body_name, - [load_function, time_rule](const TimeInfo& t, auto X, auto U, auto U_old, auto U_dot_old, auto U_dot_dot_old, - auto... inputs) { - return smith::tuple{ - -load_function(t.time(), get(X), - get(time_rule.value(t, U, U_old, U_dot_old, U_dot_dot_old)), - get(time_rule.dot(t, U, U_old, U_dot_old, U_dot_dot_old)), - get(time_rule.ddot(t, U, U_old, U_dot_old, U_dot_dot_old)), get(inputs)...), - smith::zero{}}; - }); - final_reaction_weak_form_->addBodyIntegral( - DependsOn<0, 1, 2, NUM_STATE_VARS - 1 + active_parameters...>{}, body_name, - [load_function](const TimeInfo& t, auto X, auto... inputs) { - return smith::tuple{-load_function(t.time(), get(X), get(inputs)...), smith::zero{}}; - }); - } - - /// @brief Add a body source using all trailing inputs. - template - void addBodySource(std::string body_name, BodyLoadType load_function) - { - addBodySource(DependsOn<>{}, body_name, load_function); - } - - private: - std::shared_ptr time_discretized_weak_form_; - std::shared_ptr final_reaction_weak_form_; - ImplicitNewmarkSecondOrderTimeIntegrationRule time_rule_; -}; - -} // namespace smith diff --git a/src/smith/differentiable_numerics/time_integration_rule.hpp b/src/smith/differentiable_numerics/time_integration_rule.hpp index 34eab564ad..5d6b32354f 100644 --- a/src/smith/differentiable_numerics/time_integration_rule.hpp +++ b/src/smith/differentiable_numerics/time_integration_rule.hpp @@ -112,15 +112,15 @@ class QuasiStaticRule : public TimeIntegrationRule { /// @brief get the number of states required by the rule int num_args() const override { return num_states; } - /// @brief evaluate value of the ode state as used by the integration rule template + /// @brief Return the static field value. SMITH_HOST_DEVICE auto value(const TimeInfo& /*t*/, const T1& field_new) const { return field_new; } - /// @brief evaluate time derivative discretization of the ode state as used by the integration rule template + /// @brief Return zero first derivative for a static field. SMITH_HOST_DEVICE auto dot(const TimeInfo& /*t*/, const T1& /*field_new*/) const { return zero{}; @@ -153,6 +153,65 @@ class QuasiStaticRule : public TimeIntegrationRule { } }; +/// @brief encodes rules for static postprocessing fields with zero time derivatives. +class StaticTimeIntegrationRule : public TimeIntegrationRule { + public: + static constexpr int num_states = 1; ///< number of states required by this rule (compile-time) + + int num_args() const override { return num_states; } + + /** + * @brief Return the static field value. + */ + template + SMITH_HOST_DEVICE auto value(const TimeInfo& /*t*/, const T1& field_new) const + { + return field_new; + } + + /** + * @brief Return zero first derivative for a static field. + */ + template + SMITH_HOST_DEVICE auto dot(const TimeInfo& /*t*/, const T1& /*field_new*/) const + { + return zero{}; + } + + /** + * @brief Return zero second derivative for a static field. + */ + template + SMITH_HOST_DEVICE auto ddot(const TimeInfo& /*t*/, const T1& /*field_new*/) const + { + return zero{}; + } + + /** + * @brief Return value, first derivative, and second derivative for a static field. + */ + template + SMITH_HOST_DEVICE auto interpolate(const TimeInfo& t, const T1& field_new) const + { + return std::make_tuple(value(t, field_new), dot(t, field_new), ddot(t, field_new)); + } + + FieldState corrected_value(const TimeInfo& t, const std::vector& states) const override + { + return value(t, states[0]); + } + + FieldState corrected_dot(const TimeInfo& /*t*/, const std::vector& states) const override + { + return zeroCopy(states[0]); + } + + FieldState corrected_ddot(const TimeInfo& /*t*/, const std::vector& states) const override + { + return zeroCopy(states[0]); + } +}; + /// @brief Alias for BackwardEulerFirstOrderTimeIntegrationRule for convenience. Quasi-static still should compute /// velocities (viscosities) using backward Euler. using QuasiStaticFirstOrderTimeIntegrationRule = BackwardEulerFirstOrderTimeIntegrationRule; @@ -180,7 +239,12 @@ struct ImplicitNewmarkSecondOrderTimeIntegrationRule : public TimeIntegrationRul [[maybe_unused]] const T2& field_old, [[maybe_unused]] const T3& velo_old, [[maybe_unused]] const T4& accel_old) const { - return field_new; + auto regular_value = field_new + (field_old - field_old); + auto cycle_zero_correction = field_old - field_new; + if (t.isCycleZeroEvaluation()) { + return regular_value + cycle_zero_correction; + } + return regular_value + (cycle_zero_correction - cycle_zero_correction); } /// @brief evaluate time derivative discretization of the ode state as used by the integration rule @@ -189,7 +253,12 @@ struct ImplicitNewmarkSecondOrderTimeIntegrationRule : public TimeIntegrationRul [[maybe_unused]] const T2& field_old, [[maybe_unused]] const T3& velo_old, [[maybe_unused]] const T4& accel_old) const { - return (2.0 / t.dt()) * (field_new - field_old) - velo_old; + auto regular_dot = (2.0 / t.dt()) * (field_new - field_old) - velo_old; + auto cycle_zero_correction = velo_old - regular_dot; + if (t.isCycleZeroEvaluation()) { + return regular_dot + cycle_zero_correction; + } + return regular_dot + (cycle_zero_correction - cycle_zero_correction); } /// @brief evaluate time derivative discretization of the ode state as used by the integration rule @@ -199,7 +268,12 @@ struct ImplicitNewmarkSecondOrderTimeIntegrationRule : public TimeIntegrationRul [[maybe_unused]] const T4& accel_old) const { auto dt = t.dt(); - return (4.0 / (dt * dt)) * (field_new - field_old) - (4.0 / dt) * velo_old - accel_old; + auto regular_ddot = (4.0 / (dt * dt)) * (field_new - field_old) - (4.0 / dt) * velo_old - accel_old; + auto cycle_zero_correction = accel_old - regular_ddot; + if (t.isCycleZeroEvaluation()) { + return regular_ddot + cycle_zero_correction; + } + return regular_ddot + (cycle_zero_correction - cycle_zero_correction); } /// @brief interpolate all derived quantities in one call diff --git a/src/smith/numerics/equation_solver.cpp b/src/smith/numerics/equation_solver.cpp index 70ebd6a41c..1b9cd52e1d 100644 --- a/src/smith/numerics/equation_solver.cpp +++ b/src/smith/numerics/equation_solver.cpp @@ -25,6 +25,36 @@ namespace smith { namespace { +/** + * @brief Simple solver wrapper that only applies a preconditioner. + */ +class PreconditionerOnlySolver : public mfem::IterativeSolver { + public: + PreconditionerOnlySolver(MPI_Comm mpi_comm) : mfem::IterativeSolver(mpi_comm) {} + + /// @overload + void Mult(const mfem::Vector& x, mfem::Vector& y) const override + { + if (prec) { + prec->Mult(x, y); + } else { + y = x; + } + } + + /// @overload + void SetOperator(const mfem::Operator& op) override + { + if (prec) { + prec->SetOperator(op); + } + width = op.Width(); + height = op.Height(); + } + + private: + // Note: mfem::IterativeSolver already has a 'prec' member (mfem::Solver*) +}; class SolverWithPreconditioner : public mfem::Solver { public: @@ -74,6 +104,7 @@ bool linearSolverSupportsBlockOperator(LinearSolver linear_solver) case LinearSolver::PetscCG: case LinearSolver::PetscGMRES: #endif + case LinearSolver::PrecondOnly: return true; default: return false; @@ -1355,6 +1386,9 @@ std::pair, std::unique_ptr> buildLin exit(1); break; #endif + case LinearSolver::PrecondOnly: + iter_lin_solver = std::make_unique(comm); + break; default: SLIC_ERROR_ROOT("Linear solver type not recognized."); exit(1); diff --git a/src/smith/numerics/solver_config.hpp b/src/smith/numerics/solver_config.hpp index 6f30ef8d3f..c971ecb37c 100644 --- a/src/smith/numerics/solver_config.hpp +++ b/src/smith/numerics/solver_config.hpp @@ -103,12 +103,13 @@ struct TimesteppingOptions { /// Linear solution method indicator enum class LinearSolver { - CG, /**< Conjugate gradient */ - GMRES, /**< Generalized minimal residual method */ - SuperLU, /**< SuperLU MPI-enabled direct nodal solver */ - Strumpack, /**< Strumpack MPI-enabled direct frontal solver*/ - PetscCG, /**< PETSc MPI-enabled conjugate gradient solver */ - PetscGMRES /**< PETSc MPI-enabled generalize minimal residual solver */ + CG, /**< Conjugate gradient */ + GMRES, /**< Generalized minimal residual method */ + SuperLU, /**< SuperLU MPI-enabled direct nodal solver */ + Strumpack, /**< Strumpack MPI-enabled direct frontal solver*/ + PetscCG, /**< PETSc MPI-enabled conjugate gradient solver */ + PetscGMRES, /**< PETSc MPI-enabled generalize minimal residual solver */ + PrecondOnly /**< Preconditioner application only; no Krylov iterations */ }; // _linear_solvers_end @@ -128,6 +129,8 @@ inline std::string linearName(const LinearSolver& s) return "PetscCG"; case LinearSolver::PetscGMRES: return "PetscGMRES"; + case LinearSolver::PrecondOnly: + return "PrecondOnly"; } // This cannot happen, but GCC doesn't know that return "UNKNOWN"; @@ -138,9 +141,13 @@ inline std::ostream& operator<<(std::ostream& os, LinearSolver s) { return os << /// string->value matching for optionally entering options as string in command line inline std::map linearSolverMap = { - {"CG", LinearSolver::CG}, {"GMRES", LinearSolver::GMRES}, - {"SuperLU", LinearSolver::SuperLU}, {"Strumpack", LinearSolver::Strumpack}, - {"PetscCG", LinearSolver::PetscCG}, {"PetscGMRES", LinearSolver::PetscGMRES}, + {"CG", LinearSolver::CG}, + {"GMRES", LinearSolver::GMRES}, + {"SuperLU", LinearSolver::SuperLU}, + {"Strumpack", LinearSolver::Strumpack}, + {"PetscCG", LinearSolver::PetscCG}, + {"PetscGMRES", LinearSolver::PetscGMRES}, + {"PrecondOnly", LinearSolver::PrecondOnly}, }; // Add a custom list of strings? conduit node? diff --git a/src/smith/numerics/tests/CMakeLists.txt b/src/smith/numerics/tests/CMakeLists.txt index 10e693b21a..7970beda7a 100644 --- a/src/smith/numerics/tests/CMakeLists.txt +++ b/src/smith/numerics/tests/CMakeLists.txt @@ -12,10 +12,11 @@ set(numerics_serial_test_sources test_odes.cpp test_block_preconditioner.cpp test_block_preconditioner_backend.cpp + test_linear_solver_none.cpp test_block_preconditioner_custom_operators.cpp ) -smith_add_tests( SOURCES ${numerics_serial_test_sources} +smith_add_tests(SOURCES ${numerics_serial_test_sources} DEPENDS_ON ${numerics_test_dependencies} NUM_MPI_TASKS 1) diff --git a/src/smith/numerics/tests/test_linear_solver_none.cpp b/src/smith/numerics/tests/test_linear_solver_none.cpp new file mode 100644 index 0000000000..eba3daba54 --- /dev/null +++ b/src/smith/numerics/tests/test_linear_solver_none.cpp @@ -0,0 +1,111 @@ +// Copyright (c) Lawrence Livermore National Security, LLC and +// other Smith Project Developers. See the top-level LICENSE file for +// details. +// +// SPDX-License-Identifier: (BSD-3-Clause) + +#include "gtest/gtest.h" +#include "mfem.hpp" +#include "smith/numerics/equation_solver.hpp" +#include "smith/numerics/solver_config.hpp" +#include "smith/infrastructure/application_manager.hpp" + +namespace smith { + +/** + * @brief Simple identity-like operator: A*x = x + */ +class IdentityOperator : public mfem::Operator { + public: + IdentityOperator(int size) : mfem::Operator(size) {} + void Mult(const mfem::Vector& x, mfem::Vector& y) const override { y = x; } + mfem::Operator& GetGradient(const mfem::Vector& /*x*/) const override { return const_cast(*this); } +}; + +/** + * @brief Simple diagonal operator: A*x = d * x + */ +class DiagonalOperator : public mfem::Operator { + public: + DiagonalOperator(const mfem::Vector& d) : mfem::Operator(d.Size()), d_(d) {} + void Mult(const mfem::Vector& x, mfem::Vector& y) const override + { + for (int i = 0; i < height; i++) { + y(i) = d_(i) * x(i); + } + } + mfem::Operator& GetGradient(const mfem::Vector& /*x*/) const override { return const_cast(*this); } + + private: + const mfem::Vector& d_; +}; + +TEST(LinearSolverPrecondOnly, Identity) +{ + int size = 10; + IdentityOperator op(size); + + LinearSolverOptions linear_opts; + linear_opts.linear_solver = LinearSolver::PrecondOnly; + linear_opts.preconditioner = Preconditioner::None; + + NonlinearSolverOptions nonlinear_opts; + nonlinear_opts.nonlin_solver = NonlinearSolver::Newton; + nonlinear_opts.max_iterations = 1; + + EquationSolver solver(nonlinear_opts, linear_opts, MPI_COMM_WORLD); + solver.setOperator(op); + + mfem::Vector x(size); + x = 1.0; // Initial guess + // Residual will be f(x) = x. + // x_new = x - [df/dx]^-1 * f(x) = x - I^-1 * x = 0. + + solver.solve(x); + + for (int i = 0; i < size; i++) { + EXPECT_NEAR(x(i), 0.0, 1e-12); + } +} + +TEST(LinearSolverPrecondOnly, Jacobi) +{ + int size = 10; + mfem::Vector d(size); + d = 2.0; + DiagonalOperator op(d); + + LinearSolverOptions linear_opts; + linear_opts.linear_solver = LinearSolver::PrecondOnly; + linear_opts.preconditioner = Preconditioner::None; // We'll set this manually if needed, but wait. + // Actually, Preconditioner::None with LinearSolver::PrecondOnly should be Identity. + // Let's test that first. + + NonlinearSolverOptions nonlinear_opts; + nonlinear_opts.nonlin_solver = NonlinearSolver::Newton; + nonlinear_opts.max_iterations = 1; + + EquationSolver solver(nonlinear_opts, linear_opts, MPI_COMM_WORLD); + solver.setOperator(op); + + mfem::Vector x(size); + x = 1.0; + // f(x) = 2x. df/dx = 2I. + // If LinearSolver::PrecondOnly and Preconditioner::None, it uses identity: + // x_new = x - I * (2x) = x - 2x = -x. + + solver.solve(x); + + for (int i = 0; i < size; i++) { + EXPECT_NEAR(x(i), -1.0, 1e-12); + } +} + +} // namespace smith + +int main(int argc, char* argv[]) +{ + ::testing::InitGoogleTest(&argc, argv); + smith::ApplicationManager applicationManager(argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/smith/physics/boundary_conditions/boundary_condition.hpp b/src/smith/physics/boundary_conditions/boundary_condition.hpp index bd86ee91c1..cb0e939cfb 100644 --- a/src/smith/physics/boundary_conditions/boundary_condition.hpp +++ b/src/smith/physics/boundary_conditions/boundary_condition.hpp @@ -110,6 +110,12 @@ class BoundaryCondition { */ const mfem::Array& getLocalDofList() const { return local_dofs_; } + /// @brief Returns stored coefficient. + const GeneralCoefficient& coefficient() const { return coef_; } + + /// @brief Returns constrained vector component, if any. + const std::optional& component() const { return component_; } + /** * @brief Projects the associated coefficient over a solution vector on the DOFs constrained by the boundary condition * @param[in] time The time at which to project the boundary condition diff --git a/src/smith/physics/common.hpp b/src/smith/physics/common.hpp index 729ac58782..0f184206c4 100644 --- a/src/smith/physics/common.hpp +++ b/src/smith/physics/common.hpp @@ -16,9 +16,16 @@ namespace smith { /// @brief struct storing time and timestep information struct TimeInfo { + /// @brief Evaluation mode for the current step + enum class EvaluationMode + { + Regular, ///< Normal evaluation step + CycleZero ///< Initialization or cycle zero step + }; + /// @brief constructor - TimeInfo(double t, double t_step, size_t c = 0) - : time_(std::make_pair(t, 0.0)), dt_(std::make_pair(t_step, 0.0)), cycle_(c) + TimeInfo(double t, double t_step, size_t c = 0, EvaluationMode mode = EvaluationMode::Regular) + : time_(std::make_pair(t, 0.0)), dt_(std::make_pair(t_step, 0.0)), cycle_(c), mode_(mode) { } @@ -31,10 +38,17 @@ struct TimeInfo { /// @brief accessor for cycle size_t cycle() const { return cycle_; } + /// @brief true when evaluating the startup acceleration solve. + bool isCycleZeroEvaluation() const { return mode_ == EvaluationMode::CycleZero; } + + /// @brief accessor for residual evaluation mode. + EvaluationMode mode() const { return mode_; } + private: std::pair time_; ///< time and its dual std::pair dt_; ///< timestep and its dual size_t cycle_; ///< cycle, step, iteration count + EvaluationMode mode_; ///< residual evaluation mode }; /** diff --git a/src/smith/physics/dfem_weak_form.hpp b/src/smith/physics/dfem_weak_form.hpp index 83a55d6ddd..f4659602b0 100644 --- a/src/smith/physics/dfem_weak_form.hpp +++ b/src/smith/physics/dfem_weak_form.hpp @@ -189,7 +189,8 @@ class DfemWeakForm : public WeakForm { } /// @overload - mfem::Vector residual(TimeInfo time_info, ConstFieldPtr /*shape_disp*/, const std::vector& fields, + mfem::Vector residual(const TimeInfo& time_info, ConstFieldPtr /*shape_disp*/, + const std::vector& fields, const std::vector& /*quad_fields*/ = {}) const override { dt_ = time_info.dt(); @@ -202,7 +203,7 @@ class DfemWeakForm : public WeakForm { /// @overload std::unique_ptr jacobian( - TimeInfo time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, + const TimeInfo& time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, const std::vector& /*jacobian_weights*/, const std::vector& /*quad_fields*/ = {}) const override { @@ -214,7 +215,7 @@ class DfemWeakForm : public WeakForm { } /// @overload - void jvp(TimeInfo time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, + void jvp(const TimeInfo& time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, const std::vector& /*quad_fields*/, ConstFieldPtr /*v_shape_disp*/, const std::vector& /*v_fields*/, const std::vector& /*v_quad_fields*/, DualFieldPtr /*jvp_reaction*/) const override @@ -242,7 +243,7 @@ class DfemWeakForm : public WeakForm { } /// @overload - void vjp(TimeInfo time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, + void vjp(const TimeInfo& time_info, ConstFieldPtr /*shape_disp*/, const std::vector& /*fields*/, const std::vector& /*quad_fields*/, ConstFieldPtr /*v_fields*/, DualFieldPtr /*vjp_shape_disp_sensitivity*/, const std::vector& /*vjp_sensitivities*/, const std::vector& /*vjp_quad_field_sensitivities*/) const override diff --git a/src/smith/physics/functional_objective.hpp b/src/smith/physics/functional_objective.hpp index 407f5da6d8..1df14389df 100644 --- a/src/smith/physics/functional_objective.hpp +++ b/src/smith/physics/functional_objective.hpp @@ -67,12 +67,29 @@ class FunctionalObjective, std::integer_ * @param body_name string specifying the domain to integrate over * @param qfunction a callable that returns a tuple of body-force and stress */ + template + void addBodyIntegralImpl(std::string body_name, const FuncOfTimeSpaceAndParams& qfunction, + std::integer_sequence) + { + objective_->AddDomainIntegral( + smith::Dimension{}, smith::DependsOn{}, + [this, qfunction](double /*time*/, auto X, auto... params) { return qfunction(timeInfo(), X, params...); }, + mesh_->domain(body_name)); + } + + /// @brief Add a body integral depending only on selected input fields. template void addBodyIntegral(DependsOn, std::string body_name, const FuncOfTimeSpaceAndParams& qfunction) { - objective_->AddDomainIntegral(smith::Dimension{}, smith::DependsOn{}, qfunction, - mesh_->domain(body_name)); + addBodyIntegralImpl(body_name, qfunction, std::integer_sequence{}); + } + + /// @brief Add a body integral to the objective function. + template + void addBodyIntegral(std::string body_name, const FuncOfTimeSpaceAndParams& qfunction) + { + addBodyIntegralImpl(body_name, qfunction, std::make_integer_sequence{}); } /** @@ -82,48 +99,72 @@ class FunctionalObjective, std::integer_ * @param boundary_name string specifying the boundary to integrate over * @param qfunction a callable that returns a tuple of body-force and stress */ + template + void addBoundaryIntegralImpl(std::string boundary_name, const FuncOfTimeSpaceAndParams& qfunction, + std::integer_sequence) + { + objective_->AddBoundaryIntegral( + smith::Dimension{}, smith::DependsOn{}, + [this, qfunction](double /*time*/, auto X, auto... params) { return qfunction(timeInfo(), X, params...); }, + mesh_->domain(boundary_name)); + } + + /// @brief Add a boundary integral depending only on selected input fields. template void addBoundaryIntegral(DependsOn, std::string boundary_name, const FuncOfTimeSpaceAndParams& qfunction) { - objective_->AddBoundaryIntegral(smith::Dimension{}, smith::DependsOn{}, - qfunction, mesh_->domain(boundary_name)); + addBoundaryIntegralImpl(boundary_name, qfunction, std::integer_sequence{}); + } + + /// @brief Add a boundary integral to the objective function. + template + void addBoundaryIntegral(std::string boundary_name, const FuncOfTimeSpaceAndParams& qfunction) + { + addBoundaryIntegralImpl(boundary_name, qfunction, std::make_integer_sequence{}); } /// @overload - virtual double evaluate(TimeInfo time_info, ConstFieldPtr shape_disp, + virtual double evaluate(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields) const override { - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + current_time_info_ = &time_info; - return evaluateObjective(std::make_integer_sequence{}, time_info.time(), - shape_disp, fields); + double value = evaluateObjective(std::make_integer_sequence{}, time_info.time(), + shape_disp, fields); + current_time_info_ = nullptr; + return value; } /// @overload - virtual mfem::Vector gradient(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, - size_t field_ordinal) const override + virtual mfem::Vector gradient(const TimeInfo& time_info, ConstFieldPtr shape_disp, + const std::vector& fields, size_t field_ordinal) const override { - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + current_time_info_ = &time_info; auto grads = gradientEvaluators(std::make_integer_sequence{}, time_info.time(), shape_disp, fields); auto g = smith::get(grads[field_ordinal](time_info.time(), shape_disp, fields)); - return *assemble(g); + auto assembled = assemble(g); + mfem::Vector result(assembled->Size()); + result = *assembled; + current_time_info_ = nullptr; + return result; } /// @overload - virtual mfem::Vector mesh_coordinate_gradient(TimeInfo time_info, ConstFieldPtr shape_disp, + virtual mfem::Vector mesh_coordinate_gradient(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields) const override { - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + current_time_info_ = &time_info; auto g = smith::get( (*objective_)(DifferentiateWRT<0>{}, time_info.time(), *shape_disp, *fields[parameter_indices]...)); - return *assemble(g); + auto assembled = assemble(g); + mfem::Vector result(assembled->Size()); + result = *assembled; + current_time_info_ = nullptr; + return result; } private: @@ -148,11 +189,16 @@ class FunctionalObjective, std::integer_ }...}; } - /// @brief timestep, this needs to be held here and modified for rate dependent applications. - mutable double dt_ = std::numeric_limits::max(); + /// @brief Return active TimeInfo for ScalarObjective interface evaluations. + const TimeInfo& timeInfo() const + { + SLIC_ERROR_IF(current_time_info_ == nullptr, + "FunctionalObjective integrands require evaluation through the ScalarObjective interface."); + return *current_time_info_; + } - /// @brief cycle or step or iteration. This counter is useful for certain time integrators. - mutable size_t cycle_ = 0; + /// @brief Active time information forwarded to integrands. + mutable const TimeInfo* current_time_info_ = nullptr; /// @brief primary mesh std::shared_ptr mesh_; diff --git a/src/smith/physics/functional_weak_form.hpp b/src/smith/physics/functional_weak_form.hpp index 42d6345813..2ae51a243a 100644 --- a/src/smith/physics/functional_weak_form.hpp +++ b/src/smith/physics/functional_weak_form.hpp @@ -87,7 +87,6 @@ class FunctionalWeakForm, /** * @brief Add a body integral contribution to the residual * - * // DependsOn can be indices into fields which the body integral may depend on * @tparam BodyIntegralType The type of the body integral * @param body_name The name of the registered domain over which the body integrals are evaluated. * @param integrand A function describing the body force applied. Our convention for the sign of the residual @@ -99,50 +98,55 @@ class FunctionalWeakForm, * 1. `double t` the time * 2. `tuple{tensor, isoparametric derivative} X` the spatial coordinates for the quadrature point and the * coordinate's isoparametric derivative. - * 3. `tuple{value, derivative}`, a variadic list of tuples (each with a values and spatial derivative), - * one tuple for each of the trial spaces specified in the `DependsOn<...>` argument. + * 3. `tuple{value, derivative}`, a variadic list of tuples, one for each input field. * @note The actual types of these arguments passed will be `double`, `tensor` or tuples thereof * when doing direct evaluation. When differentiating with respect to one of the inputs, its stored * values will change to `dual` numbers rather than `double`. (e.g. `tensor` becomes `tensor, * 3>`) * */ - template - void addBodyIntegral(DependsOn, std::string body_name, BodyIntegralType integrand) + template + void addBodyIntegralImpl(std::string body_name, BodyIntegralType integrand, std::integer_sequence) { - weak_form_->AddDomainIntegral(Dimension{}, DependsOn{}, integrand, - mesh_->domain(body_name)); + weak_form_->AddDomainIntegral( + Dimension{}, DependsOn{}, + [this, integrand](double /*time*/, auto X, auto... inputs) { return integrand(timeInfo(), X, inputs...); }, + mesh_->domain(body_name)); v_dot_weak_form_residual_->AddDomainIntegral( - Dimension{}, DependsOn<0, 1 + active_parameters...>{}, - [integrand](double time, auto X, auto V, auto... inputs) { - auto orig_tuple = integrand(time, X, inputs...); + Dimension{}, DependsOn<0, 1 + all_params...>{}, + [this, integrand](double /*time*/, auto X, auto V, auto... inputs) { + auto orig_tuple = integrand(timeInfo(), X, inputs...); return smith::inner(get(V), get(orig_tuple)) + smith::inner(get(V), get(orig_tuple)); }, mesh_->domain(body_name)); } - /// @overload - template - void addBodyIntegral(std::string body_name, BodyForceType body_integral) + /// @brief Add a body integral depending only on selected input fields. + template + void addBodyIntegral(DependsOn, std::string body_name, BodyIntegralType integrand) { - addBodyIntegral(DependsOn<>{}, body_name, body_integral); + addBodyIntegralImpl(body_name, integrand, std::integer_sequence{}); + } + + /// @brief Add a body integral to the weak form. + template + void addBodyIntegral(std::string body_name, BodyIntegralType integrand) + { + addBodyIntegralImpl(body_name, integrand, std::make_integer_sequence{}); } /** * @brief Add a body source (body load) to the weak form * - * @tparam active_parameters Type for indices into fields which the body integral may depend on * @tparam BodyLoadType The type of the body load function * @param body_name The name of the registered domain over which the body loads are applied. - * @param depends_on Indices into fields which the body integral may depend on * @param load_function A function describing the body force applied. * @pre load_function must be a object that can be called with the following arguments: * 1. `double t` the time * 2. `tensor X` the spatial coordinates for the quadrature point. - * 3. `value`, a variadic list of field values, one tuple for each of the trial spaces specified in the - * `DependsOn<...>` argument. + * 3. `value`, a variadic list of field values, one for each input field. * The expected return is the value of the source at X. * @note The actual types of these arguments passed will be `double`, `tensor` or tuples thereof * when doing direct evaluation. When differentiating with respect to one of the inputs, its stored @@ -151,24 +155,26 @@ class FunctionalWeakForm, * */ template + /// @param depends_on Indices of input fields passed to @p load_function. void addBodySource(DependsOn depends_on, std::string body_name, BodyLoadType load_function) { - addBodyIntegral(depends_on, body_name, [load_function](double t, auto X, auto... inputs) { - return smith::tuple{-load_function(t, get(X), get(inputs)...), smith::zero{}}; + addBodyIntegral(depends_on, body_name, [load_function](const TimeInfo& t_info, auto X, auto... inputs) { + return smith::tuple{-load_function(t_info, get(X), get(inputs)...), smith::zero{}}; }); } - /// @overload - template + /// @brief Add a body source depending on all input fields. + template void addBodySource(std::string body_name, BodyLoadType load_function) { - return addBodySource(DependsOn<>{}, body_name, load_function); + addBodyIntegral(body_name, [load_function](const TimeInfo& t_info, auto X, auto... inputs) { + return smith::tuple{-load_function(t_info, get(X), get(inputs)...), smith::zero{}}; + }); } /** * @brief Add a boundary integral term to the weak form * - * * // DependsOn can be indices into fields which the body integral may depend on * @tparam BoundaryIntegrandType The type of the boundary integral function. * @param boundary_name The name of the registered domain over which the boundary integral is applied. * @param integrand A function describing the boundary integral term to include in the weak form. @@ -180,93 +186,108 @@ class FunctionalWeakForm, * @pre integrand must be a object that can be called with the following arguments: * 1. `double t` the time * 2. `tuple{tensor, surface isoparametric derivative} X` the spatial coordinates for the quadrature point - * 3. `tuple{value, surface isoparametric derivative}`, a variadic list of tuples (each with a values and - * derivative), one tuple for each of the trial spaces specified in the `DependsOn<...>` argument. + * 3. `tuple{value, surface isoparametric derivative}`, a variadic list of tuples, one for each input field. * @note The actual types of these arguments passed will be `double`, `tensor` or tuples thereof * when doing direct evaluation. When differentiating with respect to one of the inputs, its stored * values will change to `dual` numbers rather than `double`. (e.g. `tensor` becomes `tensor, * 3>`) * */ - template - void addBoundaryIntegral(DependsOn, std::string boundary_name, BoundaryIntegrandType integrand) + template + void addBoundaryIntegralImpl(std::string boundary_name, BoundaryIntegrandType integrand, + std::integer_sequence) { - weak_form_->AddBoundaryIntegral(Dimension{}, DependsOn{}, integrand, - mesh_->domain(boundary_name)); + weak_form_->AddBoundaryIntegral( + Dimension{}, DependsOn{}, + [this, integrand](double /*time*/, auto X, auto... params) { return integrand(timeInfo(), X, params...); }, + mesh_->domain(boundary_name)); v_dot_weak_form_residual_->AddBoundaryIntegral( - Dimension{}, DependsOn<0, 1 + active_parameters...>{}, - [integrand](double t, auto X, auto V, auto... params) { - auto orig_surface_flux = integrand(t, X, params...); + Dimension{}, DependsOn<0, 1 + all_params...>{}, + [this, integrand](double /*time*/, auto X, auto V, auto... params) { + auto orig_surface_flux = integrand(timeInfo(), X, params...); return smith::inner(get(V), orig_surface_flux); }, mesh_->domain(boundary_name)); } - /// @overload + /// @brief Add a boundary integral depending only on selected input fields. + template + void addBoundaryIntegral(DependsOn, std::string boundary_name, BoundaryIntegrandType integrand) + { + addBoundaryIntegralImpl(boundary_name, integrand, std::integer_sequence{}); + } + + /// @brief Add a boundary integral to the weak form. template void addBoundaryIntegral(std::string boundary_name, const BoundaryIntegrandType& integrand) { - addBoundaryIntegral(DependsOn<>{}, boundary_name, integrand); + addBoundaryIntegralImpl(boundary_name, integrand, std::make_integer_sequence{}); } /** * @brief Add a interior boundary integral term to the weak form * - * * // DependsOn can be indices into fields which the body integral may depend on * @tparam InteriorIntegrandType The type of the interior boundary integral function. * @param interior_name The name of the registered domain over which the interior boundary integral is applied. * @param integrand A function describing the interior boundary integral term to include in the weak form. * @pre integrand must be a object that can be called with the following arguments: * 1. `double t` the time * 2. `tuple{tensor, surface isoparametric derivative} X` the spatial coordinates for the quadrature point - * 3. `tuple{value, surface isoparametric derivative}`, a variadic list of tuples (each with a values and - * derivative), one tuple for each of the trial spaces specified in the `DependsOn<...>` argument. + * 3. `tuple{value, surface isoparametric derivative}`, a variadic list of tuples, one for each input field. * @note The actual types of these arguments passed will be `double`, `tensor` or tuples thereof * when doing direct evaluation. When differentiating with respect to one of the inputs, its stored * values will change to `dual` numbers rather than `double`. (e.g. `tensor` becomes `tensor, * 3>`) * */ - template - void addInteriorBoundaryIntegral(DependsOn, std::string interior_name, - InteriorIntegrandType integrand) + template + void addInteriorBoundaryIntegralImpl(std::string interior_name, InteriorIntegrandType integrand, + std::integer_sequence) { - weak_form_->AddInteriorFaceIntegral(Dimension{}, DependsOn{}, integrand, - mesh_->domain(interior_name)); + weak_form_->AddInteriorFaceIntegral( + Dimension{}, DependsOn{}, + [this, integrand](double /*time*/, auto X, auto... params) { return integrand(timeInfo(), X, params...); }, + mesh_->domain(interior_name)); v_dot_weak_form_residual_->AddInteriorFaceIntegral( - Dimension{}, DependsOn<0, 1 + active_parameters...>{}, - [integrand](double t, auto X, auto V, auto... params) { + Dimension{}, DependsOn<0, 1 + all_params...>{}, + [this, integrand](double /*time*/, auto X, auto V, auto... params) { auto [V1, V2] = V; - auto orig_surface_flux = integrand(t, X, params...); + auto orig_surface_flux = integrand(timeInfo(), X, params...); auto [flux_pos, flux_neg] = orig_surface_flux; return smith::inner(V1, flux_pos) + smith::inner(V2, flux_neg); }, mesh_->domain(interior_name)); } - /// @overload + /// @brief Add an interior boundary integral depending only on selected input fields. + template + void addInteriorBoundaryIntegral(DependsOn, std::string interior_name, + InteriorIntegrandType integrand) + { + addInteriorBoundaryIntegralImpl(interior_name, integrand, std::integer_sequence{}); + } + + /// @brief Add an interior boundary integral to the weak form. template void addInteriorBoundaryIntegral(std::string interior_name, const InteriorIntegrandType& integrand) { - addInteriorBoundaryIntegral(DependsOn<>{}, interior_name, integrand); + addInteriorBoundaryIntegralImpl(interior_name, integrand, + std::make_integer_sequence{}); } /** * @brief Add a boundary flux term to the weak form * - * @tparam active_parameters Type for indices into fields which the body integral may depend on * @tparam BoundaryFluxType The type of the traction load - * @param depends_on Indices into fields which the body integral may depend on * @param boundary_name The name of the registered domain over which the boundary integral is applied. * @param flux_function A function describing the outward normal flux applied. * @pre flux_function must be a object that can be called with the following arguments: * 1. `double t` the time * 1. `tensor X` the spatial coordinates for the quadrature point * 3. `tensor n` the outward-facing unit normal for the quadrature point - * 4. `value`, a variadic list of tuples of field values at quadrature points, - * one for each of the trial spaces specified in the `DependsOn<...>` argument. + * 4. `value`, a variadic list of field values at quadrature points, one for each input field. * The expected return is the value of the boundary flux oriented in the sense of the outward normal. * @note The actual types of these arguments passed will be `double`, `tensor` or tuples thereof * when doing direct evaluation. When differentiating with respect to one of the inputs, its stored @@ -275,42 +296,44 @@ class FunctionalWeakForm, * */ template + /// @param depends_on Indices of input fields passed to @p flux_function. void addBoundaryFlux(DependsOn depends_on, std::string boundary_name, BoundaryFluxType flux_function) { - addBoundaryIntegral(depends_on, boundary_name, [flux_function](double t, auto X, auto... inputs) { + addBoundaryIntegral(depends_on, boundary_name, [flux_function](const TimeInfo& t_info, auto X, auto... inputs) { auto n = cross(get(X)); - return -flux_function(t, get(X), normalize(n), get(inputs)...); + return -flux_function(t_info, get(X), normalize(n), get(inputs)...); }); } - /// @overload + /// @brief Add a boundary flux depending on all input fields. template - void addBoundaryFlux(std::string boundary_name, const BoundaryFluxType& integrand) + void addBoundaryFlux(std::string boundary_name, BoundaryFluxType flux_function) { - addBoundaryFlux(DependsOn<>{}, boundary_name, integrand); + addBoundaryIntegral(boundary_name, [flux_function](const TimeInfo& t_info, auto X, auto... inputs) { + auto n = cross(get(X)); + return -flux_function(t_info, get(X), normalize(n), get(inputs)...); + }); } /// @overload - mfem::Vector residual(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + mfem::Vector residual(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, [[maybe_unused]] const std::vector& quad_fields = {}) const override { validateFields(fields, "residual"); - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + SetCurrentTimeInfoRAII clear_current_time_info_on_exit(current_time_info_, time_info); auto ret = (*weak_form_)(time_info.time(), *shape_disp, *fields[input_indices]...); return ret; } /// @overload std::unique_ptr jacobian( - TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, const std::vector& jacobian_weights, [[maybe_unused]] const std::vector& quad_fields = {}) const override { validateFields(fields, "jacobian"); - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + SetCurrentTimeInfoRAII clear_current_time_info_on_exit(current_time_info_, time_info); std::unique_ptr J; @@ -341,7 +364,7 @@ class FunctionalWeakForm, } /// @overload - void jvp(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + void jvp(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, [[maybe_unused]] const std::vector& quad_fields, [[maybe_unused]] ConstFieldPtr v_shape_disp, const std::vector& v_fields, [[maybe_unused]] const std::vector& v_quad_fields, @@ -351,8 +374,7 @@ class FunctionalWeakForm, SLIC_ERROR_IF(v_fields.size() != fields.size(), "Invalid number of field sensitivities relative to the number of fields"); - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + SetCurrentTimeInfoRAII clear_current_time_info_on_exit(current_time_info_, time_info); auto jacs = jacobianFunctions(std::make_integer_sequence{}, time_info.time(), shape_disp, fields); @@ -368,7 +390,7 @@ class FunctionalWeakForm, } /// @overload - void vjp(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + void vjp(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, [[maybe_unused]] const std::vector& quad_fields, ConstFieldPtr v_field, DualFieldPtr vjp_shape_disp_sensitivity, const std::vector& vjp_sensitivities, [[maybe_unused]] const std::vector& vjp_quad_field_sensitivities) const override @@ -377,8 +399,7 @@ class FunctionalWeakForm, SLIC_ERROR_IF(vjp_sensitivities.size() != fields.size(), "Invalid number of field sensitivities relative to the number of fields"); - dt_ = time_info.dt(); - cycle_ = time_info.cycle(); + SetCurrentTimeInfoRAII clear_current_time_info_on_exit(current_time_info_, time_info); auto vecJacs = vectorJacobianFunctions(std::make_integer_sequence{}, time_info.time(), shape_disp, v_field, fields); @@ -398,19 +419,49 @@ class FunctionalWeakForm, } } - /// @brief Accessor to get a reference to the underlying ShapeAwareFunctional in case more direct access is needed. - /// @return Reference to ShapeAwareFunctional instance. - ShapeAwareFunctional& getShapeAwareResidual() { return *weak_form_; } + protected: + template + /// @brief Forwards body integrand with dependency list covering all input parameters. + void addBodyIntegralWithAllParams(std::string body_name, BodyIntegralType integrand, + std::integer_sequence) + { + addBodyIntegralImpl(body_name, integrand, std::integer_sequence{}); + } + + template + /// @brief Forwards body source with dependency list covering all input parameters. + void addBodySourceWithAllParams(std::string body_name, BodyLoadType load_function, + std::integer_sequence) + { + (void)std::integer_sequence{}; + addBodySource(body_name, load_function); + } - /// @brief Accessor to get a reference to the underlying ShapeAwareFunctional vector-residual in case more direct - /// access is needed. - /// @return Reference to ShapeAwareFunctional instance. - ShapeAwareFunctional& getShapeAwareVectorTimesResidual() + template + /// @brief Forwards boundary integrand with dependency list covering all input parameters. + void addBoundaryIntegralWithAllParams(std::string boundary_name, BoundaryIntegrandType integrand, + std::integer_sequence) { - return *v_dot_weak_form_residual_; + addBoundaryIntegralImpl(boundary_name, integrand, std::integer_sequence{}); + } + + template + /// @brief Forwards boundary flux with dependency list covering all input parameters. + void addBoundaryFluxWithAllParams(std::string boundary_name, BoundaryFluxType flux_function, + std::integer_sequence) + { + (void)std::integer_sequence{}; + addBoundaryFlux(boundary_name, flux_function); + } + + template + /// @brief Forwards interior-boundary integrand with dependency list covering all input parameters. + void addInteriorBoundaryIntegralWithAllParams(std::string interior_name, InteriorIntegrandType integrand, + std::integer_sequence) + { + addInteriorBoundaryIntegralImpl(interior_name, integrand, std::integer_sequence{}); } - protected: /// @brief Helper to validate input spaces recursively (for constructor) template void validateInputSpaces(const SpacesT& input_mfem_spaces) const @@ -509,11 +560,41 @@ class FunctionalWeakForm, }...}; } - /// @brief timestep, this needs to be held here and modified for rate dependent applications - mutable double dt_ = std::numeric_limits::max(); + /// @brief Return active TimeInfo for WeakForm interface evaluations. + const TimeInfo& timeInfo() const + { + SLIC_ERROR_IF(current_time_info_ == nullptr, + "FunctionalWeakForm integrands require evaluation through the WeakForm interface."); + return *current_time_info_; + } + + /// @brief Scoped setter for active TimeInfo during WeakForm interface evaluations. + class SetCurrentTimeInfoRAII { + public: + /// @brief Set active TimeInfo pointer until this guard is destroyed. + /// @param[in,out] current_time_info Active TimeInfo pointer to update. + /// @param[in] time_info TimeInfo to expose through FunctionalWeakForm::timeInfo(). + SetCurrentTimeInfoRAII(const TimeInfo*& current_time_info, const TimeInfo& time_info) + : current_time_info_(current_time_info) + { + current_time_info_ = &time_info; + } + + /// @brief Clear active TimeInfo pointer. + ~SetCurrentTimeInfoRAII() { current_time_info_ = nullptr; } + + /// @brief Disable copying. + SetCurrentTimeInfoRAII(const SetCurrentTimeInfoRAII&) = delete; + + /// @brief Disable copy assignment. + SetCurrentTimeInfoRAII& operator=(const SetCurrentTimeInfoRAII&) = delete; + + private: + const TimeInfo*& current_time_info_; + }; - /// @brief cycle or step or iteration. This counter is useful for certain time integrators. - mutable size_t cycle_ = 0; + /// @brief Active time information forwarded to integrands. + mutable const TimeInfo* current_time_info_ = nullptr; /// @brief primary mesh std::shared_ptr mesh_; diff --git a/src/smith/physics/materials/green_saint_venant_thermoelastic.hpp b/src/smith/physics/materials/green_saint_venant_thermoelastic.hpp index b0f88aab27..d9f47763e7 100644 --- a/src/smith/physics/materials/green_saint_venant_thermoelastic.hpp +++ b/src/smith/physics/materials/green_saint_venant_thermoelastic.hpp @@ -20,6 +20,50 @@ auto greenStrain(const tensor& grad_u) return 0.5 * (grad_u + transpose(grad_u) + dot(transpose(grad_u), grad_u)); } +/** + * @brief Compute isotropic bulk modulus from Young's modulus and Poisson ratio. + */ +template +auto bulkModulus(EType E, double nu) +{ + return E / (3.0 * (1.0 - 2.0 * nu)); +} + +/** + * @brief Compute isotropic shear modulus from Young's modulus and Poisson ratio. + */ +template +auto shearModulus(EType E, double nu) +{ + return 0.5 * E / (1.0 + nu); +} + +/** + * @brief Compute first Piola stress for Green-Saint Venant thermoelasticity. + */ +template +auto greenSaintVenantPiola(EType E, double nu, AlphaType alpha, double theta_ref, + const tensor& grad_u, TTheta theta) +{ + const auto K = bulkModulus(E, nu); + const auto G = shearModulus(E, nu); + static constexpr auto I = Identity(); + auto F = grad_u + I; + const auto Eg = greenStrain(grad_u); + const auto trEg = tr(Eg); + const auto S = 2.0 * G * dev(Eg) + K * (trEg - static_cast(dim) * alpha * (theta - theta_ref)) * I; + return dot(F, S); +} + +/** + * @brief Compute referential Fourier heat flux. + */ +template +auto fourierHeatFlux(double kappa, const tensor& grad_theta) +{ + return -kappa * grad_theta; +} + /// @brief Green-Saint Venant isotropic thermoelastic model struct GreenSaintVenantThermoelasticMaterial { double density; ///< density @@ -56,22 +100,11 @@ struct GreenSaintVenantThermoelasticMaterial { template auto operator()(State& state, const tensor& grad_u, T2 theta, const tensor& grad_theta) const { - const double K = E / (3.0 * (1.0 - 2.0 * nu)); - const double G = 0.5 * E / (1.0 + nu); - static constexpr auto I = Identity(); - auto F = grad_u + I; const auto Eg = greenStrain(grad_u); const auto trEg = tr(Eg); - - // stress - const auto S = 2.0 * G * dev(Eg) + K * (trEg - 3.0 * alpha * (theta - theta_ref)) * I; - const auto Piola = dot(F, S); - - // internal heat source - const auto s0 = -3.0 * K * alpha * theta * (trEg - state.strain_trace); - - // heat flux - const auto q0 = -kappa * grad_theta; + const auto Piola = greenSaintVenantPiola(E, nu, alpha, theta_ref, grad_u, theta); + const auto s0 = -3.0 * bulkModulus(E, nu) * alpha * theta * (trEg - state.strain_trace); + const auto q0 = fourierHeatFlux(kappa, grad_theta); state.strain_trace = get_value(trEg); @@ -139,23 +172,12 @@ struct ParameterizedGreenSaintVenantThermoelasticMaterial { T4 thermal_expansion_scaling) const { auto [scale, unused] = thermal_expansion_scaling; - const double K = E / (3.0 * (1.0 - 2.0 * nu)); - const double G = 0.5 * E / (1.0 + nu); - static constexpr auto I = Identity(); - auto F = grad_u + I; const auto Eg = greenStrain(grad_u); const auto trEg = tr(Eg); auto alpha = alpha0 * scale; - - // stress - const auto S = 2.0 * G * dev(Eg) + K * (trEg - 3.0 * alpha * (theta - theta_ref)) * I; - const auto Piola = dot(F, S); - - // internal heat source - const auto s0 = -3.0 * K * alpha * theta * (trEg - state.strain_trace); - - // heat flux - const auto q0 = -kappa * grad_theta; + const auto Piola = greenSaintVenantPiola(E, nu, alpha, theta_ref, grad_u, theta); + const auto s0 = -3.0 * bulkModulus(E, nu) * alpha * theta * (trEg - state.strain_trace); + const auto q0 = fourierHeatFlux(kappa, grad_theta); state.strain_trace = get_value(trEg); @@ -172,11 +194,11 @@ struct ParameterizedGreenSaintVenantThermoelasticMaterial { auto calculateFreeEnergy(const tensor& grad_u, T2 theta, T3 thermal_expansion_scaling) const { auto [scale, unused] = thermal_expansion_scaling; - const double K = E / (3.0 * (1.0 - 2.0 * nu)); - const double G = 0.5 * E / (1.0 + nu); + const double K = bulkModulus(E, nu); + const double G = shearModulus(E, nu); auto strain = greenStrain(grad_u); auto trE = tr(strain); - const double alpha = alpha0 * scale; + const auto alpha = alpha0 * scale; auto psi_1 = G * squared_norm(dev(strain)) + 0.5 * K * trE * trE; using std::log; auto logT = log(theta / theta_ref); diff --git a/src/smith/physics/scalar_objective.hpp b/src/smith/physics/scalar_objective.hpp index 709bd9e36e..ed17d8ba54 100644 --- a/src/smith/physics/scalar_objective.hpp +++ b/src/smith/physics/scalar_objective.hpp @@ -41,7 +41,7 @@ class ScalarObjective { * @param fields inputs to residual operator * @return double which is the scalar objective value */ - virtual double evaluate(TimeInfo time_info, ConstFieldPtr shape_disp, + virtual double evaluate(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields) const = 0; /** @brief Virtual interface for computing objective gradient from a vector of smith::FiniteElementState* @@ -52,8 +52,8 @@ class ScalarObjective { * @param field_ordinal index for which field to take the gradient with respect to * @return mfem::Vector */ - virtual mfem::Vector gradient(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, - size_t field_ordinal) const = 0; + virtual mfem::Vector gradient(const TimeInfo& time_info, ConstFieldPtr shape_disp, + const std::vector& fields, size_t field_ordinal) const = 0; /** @brief Compute objective gradient from a vector of FiniteElementState*, using int for index * @@ -63,8 +63,8 @@ class ScalarObjective { * @param field_ordinal index for which field to take the gradient with respect to * @return mfem::Vector */ - virtual mfem::Vector gradient(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, - int field_ordinal) const + virtual mfem::Vector gradient(const TimeInfo& time_info, ConstFieldPtr shape_disp, + const std::vector& fields, int field_ordinal) const { return gradient(time_info, shape_disp, fields, static_cast(field_ordinal)); } @@ -76,7 +76,7 @@ class ScalarObjective { * @param fields inputs to residual operator * @return mfem::Vector */ - virtual mfem::Vector mesh_coordinate_gradient(TimeInfo time_info, ConstFieldPtr shape_disp, + virtual mfem::Vector mesh_coordinate_gradient(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields) const = 0; /// @brief name diff --git a/src/smith/physics/tests/test_functional_weak_form.cpp b/src/smith/physics/tests/test_functional_weak_form.cpp index 61ea04e68d..758ee9e911 100644 --- a/src/smith/physics/tests/test_functional_weak_form.cpp +++ b/src/smith/physics/tests/test_functional_weak_form.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -98,10 +99,12 @@ struct WeakFormFixture : public testing::Test { std::string surface_name = "side"; mesh->addDomainOfBoundaryElements(surface_name, smith::by_attr(1)); - f_weak_form->addBoundaryFlux(surface_name, [](double /*t*/, auto /*x*/, auto n) { return 1.0 * n; }); - f_weak_form->addBodySource(smith::DependsOn<0>{}, mesh->entireBodyName(), - [](double /*t*/, auto /*x*/, auto u) { return u; }); - f_weak_form->addBodySource(mesh->entireBodyName(), [](double /*t*/, auto x) { return 0.5 * x; }); + f_weak_form->addBoundaryFlux(surface_name, + [](auto /*t_info*/, auto /*x*/, auto n, auto... /*args*/) { return 1.0 * n; }); + f_weak_form->addBodySource(mesh->entireBodyName(), + [](auto /*t_info*/, auto /*x*/, auto u, auto... /*args*/) { return u; }); + f_weak_form->addBodySource(mesh->entireBodyName(), + [](auto /*t_info*/, auto x, auto... /*args*/) { return 0.5 * x; }); // initialize fields for testing @@ -247,6 +250,41 @@ TEST_F(WeakFormFixture, JvpConsistency) } } +TEST_F(WeakFormFixture, ForwardsOriginalTimeInfoToIntegrands) +{ + using TrialSpace = VectorSpace; + using WeakFormT = + smith::FunctionalWeakForm>; + + std::vector inputs{&states[STATE::DISP].space(), &states[STATE::VELO].space(), + ¶ms[PAR::DENSITY].space()}; + auto f_weak_form = std::make_shared("time_info_forwarding", mesh, states[STATE::DISP].space(), inputs); + + double observed_time = std::numeric_limits::quiet_NaN(); + double observed_dt = std::numeric_limits::quiet_NaN(); + size_t observed_cycle = 0; + bool observed_cycle_zero = false; + + f_weak_form->addBodySource(mesh->entireBodyName(), + [&observed_time, &observed_dt, &observed_cycle, &observed_cycle_zero]( + const smith::TimeInfo& t_info, auto x, auto... /*args*/) { + observed_time = t_info.time(); + observed_dt = t_info.dt(); + observed_cycle = t_info.cycle(); + observed_cycle_zero = t_info.isCycleZeroEvaluation(); + return 0.0 * x; + }); + + smith::TimeInfo step_time(2.0, 0.25, 7, smith::TimeInfo::EvaluationMode::CycleZero); + auto input_fields = getConstFieldPointers(states, params); + f_weak_form->residual(step_time, shape_disp.get(), input_fields); + + EXPECT_DOUBLE_EQ(observed_time, step_time.time()); + EXPECT_DOUBLE_EQ(observed_dt, step_time.dt()); + EXPECT_EQ(observed_cycle, step_time.cycle()); + EXPECT_TRUE(observed_cycle_zero); +} + int main(int argc, char* argv[]) { ::testing::InitGoogleTest(&argc, argv); diff --git a/src/smith/physics/tests/test_kinematic_objective.cpp b/src/smith/physics/tests/test_kinematic_objective.cpp index 3523fa0785..3163ea951c 100644 --- a/src/smith/physics/tests/test_kinematic_objective.cpp +++ b/src/smith/physics/tests/test_kinematic_objective.cpp @@ -13,7 +13,7 @@ #include "mpi.h" #include "mfem.hpp" -#include "smith/differentiable_numerics/time_discretized_weak_form.hpp" +#include "smith/physics/functional_weak_form.hpp" #include "smith/physics/functional_objective.hpp" #include "smith/infrastructure/application_manager.hpp" #include "smith/physics/state/state_manager.hpp" @@ -38,9 +38,9 @@ struct ConstrainedWeakFormFixture : public testing::Test { using SolidMaterial = smith::solid_mechanics::NeoHookeanWithFieldDensity; using SolidWeakFormT = - smith::TimeDiscretizedWeakForm, - smith::Parameters, smith::H1, - smith::H1, DensitySpace>>; + smith::FunctionalWeakForm, + smith::Parameters, smith::H1, + smith::H1, DensitySpace>>; enum FIELD { @@ -59,8 +59,7 @@ struct ConstrainedWeakFormFixture : public testing::Test { mat.K = 1.0; mat.G = 0.5; solid_mechanics_weak_form->addBodyIntegral( - smith::DependsOn<0>{}, mesh->entireBodyName(), - [mat](auto /*t_info*/, auto /*X*/, auto u, auto /*v*/, auto a, auto density) { + mesh->entireBodyName(), [mat](auto /*t_info*/, auto /*X*/, auto u, auto /*v*/, auto a, auto density) { typename SolidMaterial::State state; auto pk_stress = mat.pkStress(state, smith::get(u), density); return smith::tuple{smith::get(a) * mat.density(density), pk_stress}; @@ -90,8 +89,8 @@ struct ConstrainedWeakFormFixture : public testing::Test { ObjectiveT::SpacesT param_space_ptrs{&input_fields[DISP]->space(), &input_fields[DENSITY]->space()}; ObjectiveT mass_objective("mass constraining", mesh, param_space_ptrs); - mass_objective.addBodyIntegral(smith::DependsOn<1>{}, mesh->entireBodyName(), - [](double /*time*/, auto /*X*/, auto RHO) { return get(RHO); }); + mass_objective.addBodyIntegral(mesh->entireBodyName(), + [](auto /*t_info*/, auto /*X*/, auto RHO) { return get(RHO); }); double mass = mass_objective.evaluate(time_info, shape_disp.get(), objective_states); @@ -99,12 +98,9 @@ struct ConstrainedWeakFormFixture : public testing::Test { for (int i = 0; i < dim; ++i) { auto cg_objective = std::make_shared("translation" + std::to_string(i), mesh, param_space_ptrs); - cg_objective->addBodyIntegral( - smith::DependsOn<0, 1>{}, mesh->entireBodyName(), - [i](double - /*time*/, - auto X, auto U, - auto RHO) { return (get(X)[i] + get(U)[i]) * get(RHO); }); + cg_objective->addBodyIntegral(mesh->entireBodyName(), [i](auto /*t_info*/, auto X, auto U, auto RHO) { + return (get(X)[i] + get(U)[i]) * get(RHO); + }); initial_cg[i] = cg_objective->evaluate(time_info, shape_disp.get(), objective_states) / mass; constraint_evaluators.push_back(cg_objective); } @@ -112,8 +108,8 @@ struct ConstrainedWeakFormFixture : public testing::Test { for (int i = 0; i < dim; ++i) { auto center_rotation_objective = std::make_shared("rotation" + std::to_string(i), mesh, param_space_ptrs); - center_rotation_objective->addBodyIntegral(smith::DependsOn<0, 1>{}, mesh->entireBodyName(), - [i, initial_cg](double /*time*/, auto X, auto U, auto RHO) { + center_rotation_objective->addBodyIntegral(mesh->entireBodyName(), + [i, initial_cg](auto /*t_info*/, auto X, auto U, auto RHO) { auto u = get(U); auto x = get(X) + u; auto dx = x - initial_cg; diff --git a/src/smith/physics/weak_form.hpp b/src/smith/physics/weak_form.hpp index 2e766de4b3..60d85040b5 100644 --- a/src/smith/physics/weak_form.hpp +++ b/src/smith/physics/weak_form.hpp @@ -51,7 +51,8 @@ class WeakForm { * @param quad_fields vector of ConstQuadratureFieldPtr * @return mfem::Vector */ - virtual mfem::Vector residual(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + virtual mfem::Vector residual(const TimeInfo& time_info, ConstFieldPtr shape_disp, + const std::vector& fields, const std::vector& quad_fields = {}) const = 0; /** @brief Derivative of the residual with respect to specified field arguments: sum_j d{r}/d{fields}_j * @@ -65,7 +66,7 @@ class WeakForm { * {fields}_j is the jth field, {r} is the residual */ virtual std::unique_ptr jacobian( - TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, const std::vector& field_argument_tangents, const std::vector& quad_fields = {}) const = 0; @@ -81,7 +82,7 @@ class WeakForm { * @param jvp_reaction output jvps: d{r} / d{fields}_j * fieldsV[j] * nullptr fieldsV are assumed to be all zero to avoid extra calculations */ - virtual void jvp(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + virtual void jvp(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, const std::vector& quad_fields, ConstFieldPtr v_shape_disp, const std::vector& v_fields, const std::vector& v_quad_fields, DualFieldPtr jvp_reaction) const = 0; @@ -98,7 +99,7 @@ class WeakForm { * @param vjp_quadrature_sensivities output vjps, 1 per input quadrature field: v_field * d{r} / * d{quadrature_field}_j */ - virtual void vjp(TimeInfo time_info, ConstFieldPtr shape_disp, const std::vector& fields, + virtual void vjp(const TimeInfo& time_info, ConstFieldPtr shape_disp, const std::vector& fields, const std::vector& quad_fields, ConstFieldPtr v_field, DualFieldPtr vjp_shape_disp_sensitivity, const std::vector& vjp_sensitivities, const std::vector& vjp_quadrature_sensivities) const = 0;