#include #include #include #include #include #include #include #include import mean_field; import test_helpers; namespace mass_normalization_test_utils { using CoupledForm = mean_field::utils::blocks::barotropic_equilibrium_form; constexpr auto densityValue = mean_field::utils::blocks::get_value_block(mean_field::utils::blocks::density_field.mass_term); constexpr auto displacementValue = mean_field::utils::blocks::get_value_block( mean_field::utils::blocks::displacement_field.geometry_term ); constexpr auto massResidual = mean_field::utils::blocks::get_residual_block( mean_field::utils::blocks::barotropic_constant_field.mass_normalization_term ); [[nodiscard]] mean_field::operators::MassNormalizationLayout make_layout(const mean_field::fem::FEM &f) { const auto densityMap = field_dof_test_utils::make_map(*f.densityFes); const auto displacementMap = field_dof_test_utils::make_map(*f.displacementFes); const auto gravityFluxMap = field_dof_test_utils::make_map(*f.gravityFluxFes); const auto gravityPotentialMap = field_dof_test_utils::make_map(*f.gravityPotentialFes); const auto enthalpyMap = field_dof_test_utils::make_map(*f.enthalpyFes); const std::array valueSizes{ densityMap.reduced_size(), displacementMap.reduced_size(), gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), enthalpyMap.reduced_size(), 1 }; const std::array residualSizes{ gravityFluxMap.reduced_size(), gravityPotentialMap.reduced_size(), densityMap.reduced_size(), displacementMap.reduced_size(), enthalpyMap.reduced_size(), 1 }; return {valueSizes, residualSizes}; } [[nodiscard]] mfem::Vector make_density( const mean_field::fem::FEM &f, const double phase ) { mfem::ParGridFunction field(f.densityFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.91 + 0.07 * std::sin(0.83 * position(0) + phase) + 0.05 * std::cos(0.61 * position(1) - phase) + 0.03 * position(2) * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector make_constant_density( const mean_field::fem::FEM &f, const double value ) { mfem::ParGridFunction field(f.densityFes.get()); mfem::ConstantCoefficient coefficient(value); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector make_density_direction( const mean_field::fem::FEM &f, const double phase ) { mfem::ParGridFunction field(f.densityFes.get()); mfem::FunctionCoefficient coefficient([phase](const mfem::Vector &position) { return 0.19 * std::sin(0.71 * position(0) + phase) - 0.13 * std::cos(0.89 * position(1) - phase) + 0.08 * position(2); }); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector make_affine_displacement( const mean_field::fem::FEM &f, const double scale ) { mfem::ParGridFunction field(f.displacementFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(position.Size()); for (int dimension = 0; dimension < position.Size(); ++dimension) { value(dimension) = scale * position(dimension); } } ); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mfem::Vector make_displacement_direction( const mean_field::fem::FEM &f, const double scale ) { mfem::ParGridFunction field(f.displacementFes.get()); mfem::VectorFunctionCoefficient coefficient( f.mesh->Dimension(), [scale](const mfem::Vector &position, mfem::Vector &value) { value.SetSize(3); value(0) = scale * (0.07 * position(0) + 0.018 * position(1) * position(2)); value(1) = scale * (-0.05 * position(1) + 0.013 * position(0) * position(2)); value(2) = scale * (0.04 * position(2) - 0.011 * position(0) * position(1)); } ); field.ProjectCoefficient(coefficient); mfem::Vector result; field.GetTrueDofs(result); return result; } [[nodiscard]] mean_field::operators::MassNormalizationDependencies make_dependencies() { return { .discretization = {.identity = 701, .revision = 3}, .density = {.identity = 709, .revision = 5}, .displacement = {.identity = 719, .revision = 7}, .targetMass = {.identity = 727, .revision = 11} }; } [[nodiscard]] mean_field::operators::context::gravity_field::GravityFieldRevisions make_gravity_revisions( const mean_field::operators::MassNormalizationDependencies &dependencies, const std::uint64_t gravityGradientRevision = 13, const std::uint64_t gravityPotentialRevision = 17 ) { return { .discretization = {.value = dependencies.discretization.revision}, .displacement = {.value = dependencies.displacement.revision}, .density = {.value = dependencies.density.revision}, .gravity_gradient = {.value = gravityGradientRevision}, .gravity_potential = {.value = gravityPotentialRevision} }; } void prepare_gravity_context( mean_field::operators::context::gravity_field::GravityFieldLinearizationContext &context, const mean_field::fem::FEM &f, const mfem::Vector &density, const mfem::Vector &displacement, const mean_field::operators::MassNormalizationDependencies &dependencies, const std::uint64_t gravityGradientRevision = 13, const std::uint64_t gravityPotentialRevision = 17 ) { mfem::Vector gravityGradient(f.gravityFluxFes->GetTrueVSize()); gravityGradient = 0.0; mfem::Vector gravityPotential(f.gravityPotentialFes->GetTrueVSize()); gravityPotential = 0.0; context.Prepare( {.density = context.GetDensityMap().gather(density), .displacement = context.GetDisplacementMap().gather(displacement), .gravity_gradient = context.GetGravityGradientMap().gather(gravityGradient), .gravity_potential = context.GetGravityPotentialMap().gather(gravityPotential)}, make_gravity_revisions(dependencies, gravityGradientRevision, gravityPotentialRevision) ); } [[nodiscard]] double residual_value(const mean_field::operators::PreparedMassNormalizationOperator &massOperator) { mfem::Vector residual; massOperator.BuildResidual(residual); REQUIRE(residual.Size() == 1); return residual(0); } [[nodiscard]] double relative_error( const double computed, const double reference ) { return std::abs(computed - reference) / std::max(std::abs(reference), 100.0 * std::numeric_limits::epsilon()); } } // namespace mass_normalization_test_utils TEST_CASE( "Prepared Mass Normalization Has The Analytic Affine Volume Scaling", tags::barotrope_mass_normalization_analytic ) { using Operator = mean_field::operators::PreparedMassNormalizationOperator; STATIC_REQUIRE_FALSE(std::is_copy_constructible_v); STATIC_REQUIRE_FALSE(std::is_copy_assignable_v); STATIC_REQUIRE_FALSE(std::is_move_constructible_v); STATIC_REQUIRE_FALSE(std::is_move_assignable_v); mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const double densityValue = 1.37; const double targetMass = 0.73; const double affineScale = 0.086; const mfem::Vector density = mass_normalization_test_utils::make_constant_density(f, densityValue); mfem::Vector displacement(f.displacementFes->GetTrueVSize()); displacement = 0.0; auto dependencies = mass_normalization_test_utils::make_dependencies(); mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); Operator massOperator(f, *f.domainMapperStateless, gravityContext); const auto initialReport = massOperator.Prepare({.targetMass = targetMass}, dependencies); CHECK(initialReport.rebuiltStaticPlan); CHECK(initialReport.refreshedGeometry); CHECK(initialReport.refreshedDensity); CHECK(initialReport.updatedTargetMass); CHECK(initialReport.assembledResidual); const double undeformedMass = massOperator.GetCurrentMass(); const mean_field::mapping::COORDINATE_SPACE volumeCoordinates = f.has_mapping() ? mean_field::mapping::COORDINATE_SPACE::PHYSICAL : mean_field::mapping::COORDINATE_SPACE::REFERENCE; const double independentlyIntegratedMass = densityValue * mean_field::analysis::get_mesh_volume(f, volumeCoordinates, mean_field::utils::DOMAINS::STELLAR); CHECK(mass_normalization_test_utils::relative_error(undeformedMass, independentlyIntegratedMass) < 1.0e-12); CHECK( mass_normalization_test_utils::relative_error( mass_normalization_test_utils::residual_value(massOperator), undeformedMass - targetMass ) < 2.0e-15 ); displacement = mass_normalization_test_utils::make_affine_displacement(f, affineScale); ++dependencies.displacement.revision; mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); const auto deformedReport = massOperator.Prepare({.targetMass = targetMass}, dependencies); CHECK_FALSE(deformedReport.rebuiltStaticPlan); CHECK(deformedReport.refreshedGeometry); CHECK_FALSE(deformedReport.refreshedDensity); const double expectedScale = std::pow(1.0 + affineScale, 3); const double measuredScale = massOperator.GetCurrentMass() / undeformedMass; INFO("Expected affine mass scale = " << expectedScale); INFO("Measured affine mass scale = " << measuredScale); CHECK(mass_normalization_test_utils::relative_error(measuredScale, expectedScale) < 5e-7); } TEST_CASE( "Prepared Mass Normalization Density Jacobian Matches Centered Difference", tags::barotrope_mass_normalization_jacobian &tags::accuracy ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.31); const mfem::Vector densityDirection = mass_normalization_test_utils::make_density_direction(f, 0.67); const mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.43); auto dependencies = mass_normalization_test_utils::make_dependencies(); mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.23}, dependencies); const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); mfem::Vector analyticAction; massOperator.ApplyDensityJacobianAction(reducedDensityDirection, analyticAction); constexpr double epsilon = 1.0e-3; mfem::Vector densityPlus(density); densityPlus.Add(epsilon, densityDirection); ++dependencies.density.revision; mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, densityPlus, displacement, dependencies); massOperator.Prepare({.targetMass = 1.23}, dependencies); const double residualPlus = mass_normalization_test_utils::residual_value(massOperator); mfem::Vector densityMinus(density); densityMinus.Add(-epsilon, densityDirection); ++dependencies.density.revision; mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, densityMinus, displacement, dependencies); massOperator.Prepare({.targetMass = 1.23}, dependencies); const double residualMinus = mass_normalization_test_utils::residual_value(massOperator); const double finiteDifference = (residualPlus - residualMinus) / (2.0 * epsilon); INFO("Density action = " << analyticAction(0)); INFO("Density centered difference = " << finiteDifference); CHECK(mass_normalization_test_utils::relative_error(analyticAction(0), finiteDifference) < 3.0e-8); } TEST_CASE( "Prepared Mass Normalization Geometry Jacobian Matches Centered Difference", tags::barotrope_mass_normalization_jacobian &tags::geometry ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.37); mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.51); const mfem::Vector displacementDirection = mass_normalization_test_utils::make_displacement_direction(f, -0.79); auto dependencies = mass_normalization_test_utils::make_dependencies(); mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.11}, dependencies); const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); mfem::Vector analyticAction; massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, analyticAction); constexpr double epsilon = 1.0e-6; mfem::Vector displacementPlus(displacement); displacementPlus.Add(epsilon, displacementDirection); ++dependencies.displacement.revision; mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacementPlus, dependencies); massOperator.Prepare({.targetMass = 1.11}, dependencies); const double residualPlus = mass_normalization_test_utils::residual_value(massOperator); mfem::Vector displacementMinus(displacement); displacementMinus.Add(-epsilon, displacementDirection); ++dependencies.displacement.revision; mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacementMinus, dependencies); massOperator.Prepare({.targetMass = 1.11}, dependencies); const double residualMinus = mass_normalization_test_utils::residual_value(massOperator); const double finiteDifference = (residualPlus - residualMinus) / (2.0 * epsilon); INFO("Geometry action = " << analyticAction(0)); INFO("Geometry centered difference = " << finiteDifference); CHECK(mass_normalization_test_utils::relative_error(analyticAction(0), finiteDifference) < 2.0e-7); } TEST_CASE( "Prepared Mass Normalization Selectively Refreshes Its Cached State", tags::barotrope_mass_normalization_context &tags::integration ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.29); mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.41); auto dependencies = mass_normalization_test_utils::make_dependencies(); std::uint64_t gravityPotentialRevision = 17; mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context( gravityContext, f, density, displacement, dependencies, 13, gravityPotentialRevision ); mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.0}, dependencies); const std::uint64_t preparationCount = massOperator.GetPreparationCount(); const auto repeated = massOperator.Prepare({.targetMass = 1.0}, dependencies); CHECK_FALSE(repeated.DidAnyWork()); CHECK(massOperator.GetPreparationCount() == preparationCount); ++gravityPotentialRevision; mass_normalization_test_utils::prepare_gravity_context( gravityContext, f, density, displacement, dependencies, 13, gravityPotentialRevision ); const auto potentialOnly = massOperator.Prepare({.targetMass = 1.0}, dependencies); CHECK_FALSE(potentialOnly.DidAnyWork()); const double residualBeforeTargetChange = mass_normalization_test_utils::residual_value(massOperator); const double massBeforeTargetChange = massOperator.GetCurrentMass(); ++dependencies.targetMass.revision; const auto targetOnly = massOperator.Prepare({.targetMass = 1.4}, dependencies); CHECK(targetOnly.updatedTargetMass); CHECK(targetOnly.assembledResidual); CHECK_FALSE(targetOnly.rebuiltStaticPlan); CHECK_FALSE(targetOnly.refreshedGeometry); CHECK_FALSE(targetOnly.refreshedDensity); CHECK(massOperator.GetCurrentMass() == massBeforeTargetChange); CHECK( mass_normalization_test_utils::relative_error( mass_normalization_test_utils::residual_value(massOperator) - residualBeforeTargetChange, -0.4 ) < 2.0e-15 ); const double massBeforeDensityChange = massOperator.GetCurrentMass(); density = mass_normalization_test_utils::make_density(f, 0.83); ++dependencies.density.revision; mass_normalization_test_utils::prepare_gravity_context( gravityContext, f, density, displacement, dependencies, 13, gravityPotentialRevision ); const auto densityOnly = massOperator.Prepare({.targetMass = 1.4}, dependencies); CHECK(densityOnly.refreshedDensity); CHECK(densityOnly.assembledResidual); CHECK_FALSE(densityOnly.refreshedGeometry); CHECK(massOperator.GetCurrentMass() != massBeforeDensityChange); displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.87); ++dependencies.displacement.revision; mass_normalization_test_utils::prepare_gravity_context( gravityContext, f, density, displacement, dependencies, 13, gravityPotentialRevision ); const auto geometryOnly = massOperator.Prepare({.targetMass = 1.4}, dependencies); CHECK(geometryOnly.refreshedGeometry); CHECK(geometryOnly.assembledResidual); CHECK_FALSE(geometryOnly.refreshedDensity); } TEST_CASE( "Compiled Fixed Total Mass Is Exactly Equivalent To The Legacy Mass State Adapter", tags::fixed_total_mass_constraint ) { using Operator = mean_field::operators::PreparedFixedMass; STATIC_CHECK(mean_field::operators::PreparedConstraint); mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.53); const mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.37); const mfem::Vector densityDirection = mass_normalization_test_utils::make_density_direction(f, -0.61); const mfem::Vector displacementDirection = mass_normalization_test_utils::make_displacement_direction(f, 0.43); const auto dependencies = mass_normalization_test_utils::make_dependencies(); mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); Operator legacy(f, *f.domainMapperStateless, gravityContext); Operator compiled(f, *f.domainMapperStateless, gravityContext); constexpr double targetMass = 1.31; const mean_field::models::CompiledFixedMass fixedMass = mean_field::models::compileConstraint( mean_field::models::FixedTotalMass{mean_field::dimensions::MassValue{targetMass}} ); const auto legacyReport = legacy.Prepare({.targetMass = targetMass}, dependencies); const auto compiledReport = compiled.Prepare(fixedMass, dependencies); CHECK(legacyReport == compiledReport); CHECK(legacy.GetPreparationCount() == compiled.GetPreparationCount()); CHECK(legacy.GetCurrentMass() == compiled.GetCurrentMass()); CHECK(legacy.GetTargetMass() == compiled.GetTargetMass()); CHECK( mass_normalization_test_utils::residual_value(legacy) == mass_normalization_test_utils::residual_value(compiled) ); const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); mfem::Vector legacyAction; mfem::Vector compiledAction; legacy.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, legacyAction); compiled.ApplyJacobian( {.densityVariation = reducedDensityDirection, .displacementVariation = reducedDisplacementDirection}, compiledAction ); REQUIRE(legacyAction.Size() == 1); REQUIRE(compiledAction.Size() == 1); CHECK(legacyAction(0) == compiledAction(0)); CHECK(legacy.GetActionStatistics().completeApplications == compiled.GetActionStatistics().completeApplications); } TEST_CASE( "Prepared Mass Normalization Complete Action And Coupled Routing Are Exact", tags::barotrope_mass_normalization_jacobian ) { mean_field::utils::Args args = test_utils::setup_args(); mean_field::fem::FEM f = mean_field::fem::setup_fem(args.mesh_file, args, 0); REQUIRE(f.okay()); const mfem::Vector density = mass_normalization_test_utils::make_density(f, 0.47); const mfem::Vector displacement = mass_normalization_test_utils::make_displacement_direction(f, 0.57); const mfem::Vector densityDirection = mass_normalization_test_utils::make_density_direction(f, 0.71); const mfem::Vector displacementDirection = mass_normalization_test_utils::make_displacement_direction(f, -0.63); const auto dependencies = mass_normalization_test_utils::make_dependencies(); mean_field::operators::context::gravity_field::GravityFieldLinearizationContext gravityContext( f, *f.domainMapperStateless ); mass_normalization_test_utils::prepare_gravity_context(gravityContext, f, density, displacement, dependencies); mean_field::operators::PreparedMassNormalizationOperator massOperator(f, *f.domainMapperStateless, gravityContext); massOperator.Prepare({.targetMass = 1.19}, dependencies); const mfem::Vector reducedDensityDirection = gravityContext.GetDensityMap().gather(densityDirection); const mfem::Vector reducedDisplacementDirection = gravityContext.GetDisplacementMap().gather(displacementDirection); mfem::Vector densityAction; mfem::Vector displacementAction; mfem::Vector completeAction; massOperator.ApplyDensityJacobianAction(reducedDensityDirection, densityAction); massOperator.ApplyDisplacementJacobianAction(reducedDisplacementDirection, displacementAction); massOperator.ApplyCompleteJacobianAction(reducedDensityDirection, reducedDisplacementDirection, completeAction); CHECK( mass_normalization_test_utils::relative_error(completeAction(0), densityAction(0) + displacementAction(0)) < 2.0e-15 ); const auto layout = mass_normalization_test_utils::make_layout(f); mean_field::operators::PreparedMassNormalizationJacobianOperator adapter(layout, massOperator); CHECK(layout.size(mass_normalization_test_utils::densityValue) == gravityContext.GetDensityMap().reduced_size()); CHECK( layout.size(mass_normalization_test_utils::displacementValue) == gravityContext.GetDisplacementMap().reduced_size() ); CHECK(gravityContext.GetDensityMap().inactive_size() > 0); mfem::Vector direction(layout.value_offsets().Last()); direction = 0.0; for (int entry = 0; entry < reducedDensityDirection.Size(); ++entry) { direction(layout.offset(mass_normalization_test_utils::densityValue) + entry) = reducedDensityDirection(entry); } for (int entry = 0; entry < reducedDisplacementDirection.Size(); ++entry) { direction(layout.offset(mass_normalization_test_utils::displacementValue) + entry) = reducedDisplacementDirection(entry); } mfem::Vector coupledAction; adapter.Mult(direction, coupledAction); const int massOffset = layout.offset(mass_normalization_test_utils::massResidual); REQUIRE(coupledAction.Size() == layout.residual_offsets().Last()); CHECK(coupledAction(massOffset) == completeAction(0)); for (int entry = 0; entry < coupledAction.Size(); ++entry) { if (entry != massOffset) { CHECK(coupledAction(entry) == 0.0); } } mfem::Vector residualDual(layout.residual_offsets().Last()); residualDual = 0.0; residualDual(massOffset) = -0.83; mfem::Vector stateDual; adapter.MultTranspose(residualDual, stateDual); REQUIRE(stateDual.Size() == direction.Size()); const double forwardPairing = coupledAction * residualDual; const double transposePairing = direction * stateDual; CHECK(mass_normalization_test_utils::relative_error(transposePairing, forwardPairing) < 2.0e-12); CHECK(massOperator.GetActionStatistics().transposeApplications == 1); CHECK(&massOperator.GetFEM() == &f); CHECK(&massOperator.GetGravityContext() == &gravityContext); CHECK(adapter.GetLayout().residual_offsets().Last() == layout.residual_offsets().Last()); }