#include #include #include #include #include #include #include #include "serif/backend/mfem/geometry/surface/pressure_boundary.hpp" #include "serif/tests/surface/boundary_fixture.hpp" #include "serif/utils/dependencies/computation_cache.hpp" namespace { namespace test = serif::tests::surface; namespace fixture = test::boundary; namespace physics = serif::geometry::surface; namespace boundary = physics::backend::mfem; namespace dependencies = serif::utils::dependencies; namespace coefficients = serif::operators::backend::mfem; using fixture::Vector; using Status = dependencies::PreparationStatus; using Linearization = physics::keys::Linearization; using Response = physics::keys::EOSResponse; using Catch::Matchers::WithinAbs; /** @brief Track physical EOS coefficients while excluding observational evaluation counters. */ [[nodiscard]] auto eos_input(const test::GeneralEOS& model) { return dependencies::borrow(model, dependencies::track_by([](const test::GeneralEOS& eos) { return eos.scale; })); } /** @brief The ideal-gas fixture has no mutable physical parameters. */ [[nodiscard]] auto eos_input(const test::GasEOS& model) { return dependencies::borrow(model, dependencies::track_by([](const test::GasEOS&) { return 0; })); } /** @brief Make the prescribed thermodynamic pressure explicit in every candidate. */ [[nodiscard]] physics::IsobaricSurface pressure(const double value) { return physics::IsobaricSurface{test::quantities::PressureValue{value}}; } /** @brief Bind the composition-dependent EOS to three distinct finite-element fields. */ [[nodiscard]] auto composition_state(const fixture::Fixture& mesh, const Vector& enthalpy, const Vector& entropy, const Vector& composition) { return coefficients::make_term_coefficients(coefficients::bind(mesh.enthalpy, enthalpy), coefficients::bind(mesh.entropy, entropy), coefficients::bind(mesh.composition, composition)); } /** @brief Check the same nonlinear gas surface with either carrier-resolved or direct-pressure residuals. */ template void check_gas_rows() { fixture::Fixture mesh; test::GasEOS model; Vector density = fixture::sequence(mesh.density.size(), 0.6, 0.1); Vector temperature = fixture::sequence(mesh.temperature.size(), 1.3, 0.2); const Vector density_direction = fixture::sequence(density.Size(), 0.07, 0.3); const Vector temperature_direction = fixture::sequence(temperature.Size(), -0.11, 0.4); const auto state = coefficients::make_term_coefficients(coefficients::bind(mesh.density, density), coefficients::bind(mesh.temperature, temperature)); const auto direction = coefficients::make_term_coefficients(coefficients::bind(mesh.density, density_direction), coefficients::bind(mesh.temperature, temperature_direction)); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.8), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const Vector sampled_temperature = fixture::reference_samples(mesh.density, *selected->selected_rows, mesh.temperature, temperature); const Vector sampled_direction = fixture::reference_samples(mesh.density, *selected->selected_rows, mesh.temperature, temperature_direction); const auto& physics = cache.template get(); Vector residual(density.Size()), action(density.Size()); residual = 4.0; action = -3.0; boundary::apply_residual_rows(*selected, physics, residual); boundary::apply_jacobian_rows(*selected, physics, direction, action); const Vector mask = fixture::boundary_mask(mesh.density); for (int index = 0; index < selected->selected_rows->size(); ++index) { const int row = selected->selected_rows->reduced_dofs()[index]; const double local_temperature = sampled_temperature[index]; const double expected_residual = Formulation::resolves_carrier ? density[row] - 0.8 / local_temperature : density[row] * local_temperature - 0.8; const double expected_action = Formulation::resolves_carrier ? density_direction[row] + 0.8 * sampled_direction[index] / (local_temperature * local_temperature) : local_temperature * density_direction[row] + density[row] * sampled_direction[index]; CHECK_THAT(residual[row], WithinAbs(expected_residual, 1.0e-13)); CHECK_THAT(action[row], WithinAbs(expected_action, 1.0e-13)); } for (int row = 0; row < mask.Size(); ++row) { if (mask[row] == 0.0) { CHECK(residual[row] == 4.0); CHECK(action[row] == -3.0); action[row] = 0.0; } } const Vector dual = fixture::sequence(density.Size(), 0.4, 0.8); Vector density_force(density.Size()), temperature_force(temperature.Size()); density_force = 0.0; temperature_force = 0.0; boundary::add_jacobian_transpose_rows(*selected, physics, dual, density_force); boundary::add_jacobian_transpose_rows(*selected, physics, dual, temperature_force); const double forward_work = action * dual; const double reverse_work = density_direction * density_force + temperature_direction * temperature_force; CHECK_THAT(forward_work, WithinAbs(reverse_work, 2.0e-13 * std::max(1.0, std::abs(forward_work)))); constexpr double step = 1.0e-7; const Vector original_density(density), original_temperature(temperature); Vector plus(density.Size()), minus(density.Size()); plus = 0.0; minus = 0.0; density.Add(step, density_direction); temperature.Add(step, temperature_direction); REQUIRE(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.8), state))); boundary::apply_residual_rows(*selected, cache.template get(), plus); density = original_density; temperature = original_temperature; density.Add(-step, density_direction); temperature.Add(-step, temperature_direction); REQUIRE(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.8), state))); boundary::apply_residual_rows(*selected, cache.template get(), minus); for (const int row : selected->selected_rows->reduced_dofs()) { CHECK_THAT((plus[row] - minus[row]) / (2.0 * step), WithinAbs(action[row], 2.0e-9)); } } } TEST_CASE("General EOS pressure rows sample every field in its own finite-element basis", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const Vector enthalpy_direction = fixture::sequence(enthalpy.Size(), 0.7, 0.2); const Vector entropy_direction = fixture::sequence(entropy.Size(), -0.2, 0.4); const Vector composition_direction = fixture::sequence(composition.Size(), 0.05, 0.6); const auto state = composition_state(mesh, enthalpy, entropy, composition); const auto direction = composition_state(mesh, enthalpy_direction, entropy_direction, composition_direction); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); CHECK(model.evaluations == 0); REQUIRE(cache.try_prepare(inputs)); const auto& accepted = cache.get(); const auto& rows = *selected->selected_rows; REQUIRE(rows.size() > 0); REQUIRE(rows.size() < enthalpy.Size()); const Vector mask = fixture::boundary_mask(mesh.enthalpy); CHECK(rows.size() == static_cast(mask.Sum())); const Vector sampled_entropy = fixture::reference_samples(mesh.enthalpy, rows, mesh.entropy, entropy); const Vector sampled_composition = fixture::reference_samples(mesh.enthalpy, rows, mesh.composition, composition); const Vector sampled_entropy_direction = fixture::reference_samples(mesh.enthalpy, rows, mesh.entropy, entropy_direction); const Vector sampled_composition_direction = fixture::reference_samples(mesh.enthalpy, rows, mesh.composition, composition_direction); Vector residual(enthalpy.Size()), action(enthalpy.Size()); residual = 7.0; action = -4.0; boundary::apply_residual_rows(*selected, accepted, residual); boundary::apply_jacobian_rows(*selected, accepted, direction, action); for (int index = 0; index < rows.size(); ++index) { const int row = rows.reduced_dofs()[index]; CHECK(mask[row] == 1.0); CHECK_THAT(residual[row], WithinAbs(enthalpy[row] - (0.8 + 3.0 * sampled_entropy[index] + 5.0 * sampled_composition[index]), 1.0e-13)); CHECK_THAT(action[row], WithinAbs(enthalpy_direction[row] - 3.0 * sampled_entropy_direction[index] - 5.0 * sampled_composition_direction[index], 1.0e-13)); } for (int row = 0; row < mask.Size(); ++row) { if (mask[row] == 0.0) { CHECK(residual[row] == 7.0); CHECK(action[row] == -4.0); action[row] = 0.0; } } const Vector dual = fixture::sequence(enthalpy.Size(), 0.3, 0.9); Vector enthalpy_force(enthalpy.Size()), entropy_force(entropy.Size()), composition_force(composition.Size()), volume_dual; enthalpy_force = 0.0; entropy_force = 0.0; composition_force = 0.0; boundary::mask_residual_dual(*selected, dual, volume_dual); boundary::add_jacobian_transpose_rows(*selected, accepted, dual, enthalpy_force); boundary::add_jacobian_transpose_rows(*selected, accepted, dual, entropy_force); boundary::add_jacobian_transpose_rows(*selected, accepted, dual, composition_force); const double forward_work = dual * action; const double reverse_work = enthalpy_direction * enthalpy_force + entropy_direction * entropy_force + composition_direction * composition_force; CHECK_THAT(forward_work, WithinAbs(reverse_work, 2.0e-13 * std::max(1.0, std::abs(forward_work)))); for (int row = 0; row < dual.Size(); ++row) { CHECK(volume_dual[row] == (mask[row] != 0.0 ? 0.0 : dual[row])); } Vector aliased_direction(enthalpy_direction); const auto aliased_state = composition_state(mesh, aliased_direction, entropy_direction, composition_direction); boundary::apply_jacobian_rows(*selected, accepted, aliased_state, aliased_direction); Vector aliased_dual(dual); boundary::add_jacobian_transpose_rows(*selected, accepted, aliased_dual, aliased_dual); for (int row = 0; row < enthalpy.Size(); ++row) { CHECK(aliased_direction[row] == (mask[row] != 0.0 ? action[row] : enthalpy_direction[row])); CHECK_THAT(aliased_dual[row], WithinAbs(dual[row] + enthalpy_force[row], 1.0e-15)); } // Mixed-order FE interpolation adds roundoff before residual subtraction; a larger step retains the original absolute tolerance. // The local EOS regression separately preserves MeanField's original 1e-7 step. constexpr double step = 1.0e-6; const Vector original_enthalpy(enthalpy), original_entropy(entropy), original_composition(composition); Vector plus(enthalpy.Size()), minus(enthalpy.Size()); plus = 0.0; minus = 0.0; enthalpy.Add(step, enthalpy_direction); entropy.Add(step, entropy_direction); composition.Add(step, composition_direction); REQUIRE(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state))); boundary::apply_residual_rows(*selected, cache.get(), plus); enthalpy = original_enthalpy; entropy = original_entropy; composition = original_composition; enthalpy.Add(-step, enthalpy_direction); entropy.Add(-step, entropy_direction); composition.Add(-step, composition_direction); REQUIRE(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state))); boundary::apply_residual_rows(*selected, cache.get(), minus); for (const int row : rows.reduced_dofs()) { CHECK_THAT((plus[row] - minus[row]) / (2.0 * step), WithinAbs(action[row], 2.0e-9)); } } TEST_CASE("A continuous density field enforces isobaric rows without enthalpy", "[surface][unit][boundary]") { SECTION("Density from pressure and temperature") { check_gas_rows>(); } SECTION("Pressure directly from density and temperature") { check_gas_rows>(); } } TEST_CASE("Uniform EOS inputs scatter their transpose contributions into one global scalar", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GasEOS model; Vector density = fixture::sequence(mesh.density.size(), 0.6, 0.1); Vector temperature(1), density_direction = fixture::sequence(density.Size(), 0.1, 0.4), temperature_direction(1); temperature[0] = 1.4; temperature_direction[0] = -0.2; const auto state = coefficients::make_term_coefficients(coefficients::bind(mesh.density, density), coefficients::bind(mesh.uniform_temperature, temperature)); const auto direction = coefficients::make_term_coefficients(coefficients::bind(mesh.density, density_direction), coefficients::bind(mesh.uniform_temperature, temperature_direction)); using Formulation = fixture::DensityFormulation; const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.8), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const auto& accepted = cache.get(); Vector residual(density.Size()), action(density.Size()), density_force(density.Size()), temperature_force(1); residual = 0.0; action = 0.0; density_force = 0.0; temperature_force = 0.0; boundary::apply_residual_rows(*selected, accepted, residual); boundary::apply_jacobian_rows(*selected, accepted, direction, action); const Vector dual = fixture::sequence(density.Size(), 0.4, 0.8); boundary::add_jacobian_transpose_rows(*selected, accepted, dual, density_force); boundary::add_jacobian_transpose_rows(*selected, accepted, dual, temperature_force); double expected_temperature_force = 0.0; for (const int row : selected->selected_rows->reduced_dofs()) { CHECK_THAT(residual[row], WithinAbs(density[row] - 0.8 / temperature[0], 1.0e-13)); expected_temperature_force += 0.8 / (temperature[0] * temperature[0]) * dual[row]; } CHECK_THAT(temperature_force[0], WithinAbs(expected_temperature_force, 1.0e-13)); CHECK_THAT(action * dual, WithinAbs(density_direction * density_force + temperature_direction * temperature_force, 2.0e-13)); } TEST_CASE("Boundary field sampling and EOS work have distinct dependency lifetimes", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const auto state = composition_state(mesh, enthalpy, entropy, composition); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); CHECK(model.evaluations == 0); REQUIRE(cache.try_prepare(inputs)); const int evaluations = model.evaluations; REQUIRE(evaluations > 0); const auto repeated = cache.try_prepare(inputs); REQUIRE(repeated); CHECK(repeated->computed_count() == 0); enthalpy[selected->selected_rows->reduced_dofs()[0]] += 0.1; const auto carrier_changed = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state)); REQUIRE(carrier_changed); CHECK(carrier_changed->status>() == Status::computed); CHECK(carrier_changed->status>() == Status::reused); CHECK(carrier_changed->status() == Status::reused); CHECK(carrier_changed->status() == Status::computed); CHECK(model.evaluations == evaluations); entropy *= 1.1; const auto entropy_changed = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state)); REQUIRE(entropy_changed); CHECK(entropy_changed->status>() == Status::computed); CHECK(entropy_changed->status() == Status::computed); const auto pressure_changed = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.5), state)); REQUIRE(pressure_changed); CHECK(pressure_changed->status() == Status::computed); CHECK(pressure_changed->status>() == Status::reused); model.scale = 1.2; const auto eos_changed = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.5), state)); REQUIRE(eos_changed); CHECK(eos_changed->status() == Status::computed); CHECK(eos_changed->status>() == Status::reused); CHECK(cache.get()[0].field_derivatives.get() == -3.0 * model.scale); } TEST_CASE("Direct-pressure boundary changes reuse EOS samples when only prescribed pressure changes", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GasEOS model; Vector density = fixture::sequence(mesh.density.size(), 0.6, 0.1); Vector temperature = fixture::sequence(mesh.temperature.size(), 1.3, 0.2); const auto state = coefficients::make_term_coefficients(coefficients::bind(mesh.density, density), coefficients::bind(mesh.temperature, temperature)); using Formulation = fixture::PressureFormulation<>; const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.8), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const int evaluations = model.evaluations; const double previous = cache.get()[0].residual; const auto changed = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.9), state)); REQUIRE(changed); CHECK(changed->status() == Status::reused); CHECK(changed->status() == Status::computed); CHECK(model.evaluations == evaluations); CHECK_THAT(cache.get()[0].residual, WithinAbs(previous - 0.1, 1.0e-15)); } TEST_CASE("A rejected boundary candidate hides old results and permits a valid retry", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const auto state = composition_state(mesh, enthalpy, entropy, composition); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const double previous = cache.get()[0].residual; const Vector original_entropy(entropy); entropy = std::numeric_limits::quiet_NaN(); const auto rejected = cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state)); REQUIRE_FALSE(rejected); CHECK_FALSE(cache.ready()); CHECK_THROWS_AS(cache.get(), std::logic_error); entropy = original_entropy; REQUIRE(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state))); CHECK(cache.get()[0].residual == previous); } TEST_CASE("Pressure boundaries reject stale and unrelated coefficient bindings before publishing rows", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const auto state = composition_state(mesh, enthalpy, entropy, composition); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); SECTION("A same-sized binding cannot silently replace the compiled field") { fixture::finite_elements::TermBinding other_entropy(mesh.linear_space); const auto other_state = coefficients::make_term_coefficients(coefficients::bind(mesh.enthalpy, enthalpy), coefficients::bind(other_entropy, entropy), coefficients::bind(mesh.composition, composition)); const auto other_inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), other_state); CHECK_THROWS_AS(cache.try_prepare(other_inputs), std::invalid_argument); CHECK_FALSE(cache.ready()); REQUIRE(cache.try_prepare(inputs)); } SECTION("The candidate vector must match its finite-element coefficients") { Vector short_entropy(entropy.Size() - 1); short_entropy = 0.2; const auto other_state = composition_state(mesh, enthalpy, short_entropy, composition); CHECK_THROWS_AS(cache.try_prepare(boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), other_state)), std::invalid_argument); CHECK_FALSE(cache.ready()); REQUIRE(cache.try_prepare(inputs)); } SECTION("Refinement invalidates traces even if all coefficients are unchanged") { mesh.mesh.UniformRefinement(); CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error); CHECK_FALSE(cache.ready()); CHECK_THROWS_AS(cache.get(), std::logic_error); } SECTION("Boundary labels invalidate the selected pressure rows without a topology revision") { const int previous = mesh.mesh.GetBdrAttribute(0); mesh.mesh.GetBdrElement(0)->SetAttribute(previous == 1 ? 2 : 1); mesh.mesh.SetAttributes(); CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error); CHECK_FALSE(cache.ready()); mesh.mesh.GetBdrElement(0)->SetAttribute(previous); mesh.mesh.SetAttributes(); REQUIRE(cache.try_prepare(inputs)); } SECTION("Material labels invalidate the supported coefficient binding") { mesh.mesh.GetElement(0)->SetAttribute(2); mesh.mesh.SetAttributes(); CHECK_THROWS_AS(cache.try_prepare(inputs), std::logic_error); CHECK_FALSE(cache.ready()); mesh.mesh.GetElement(0)->SetAttribute(1); mesh.mesh.SetAttributes(); REQUIRE(cache.try_prepare(inputs)); } SECTION("Curved coordinate edits preserve canonical finite-element traces") { const double previous = cache.get()[0].residual; const int evaluations = model.evaluations; const Vector original_nodes(*mesh.mesh.GetNodes()); ::mfem::VectorFunctionCoefficient warp(3, [](const Vector& position, Vector& mapped) { mapped = position; mapped[0] += 0.04 * position[1] * position[1]; mapped[2] += 0.03 * position[0] * position[1]; }); mesh.mesh.Transform(warp); Vector coordinate_change(*mesh.mesh.GetNodes()); coordinate_change -= original_nodes; REQUIRE(coordinate_change.Norml2() > 0.0); const auto reused = cache.try_prepare(inputs); REQUIRE(reused); CHECK(reused->computed_count() == 0); CHECK(model.evaluations == evaluations); CHECK(cache.get()[0].residual == previous); } } TEST_CASE("Pressure row actions reject malformed buffers before replacing any entries", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const auto state = composition_state(mesh, enthalpy, entropy, composition); const auto selected = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(selected, eos_input(model), pressure(0.4), state); auto cache = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(cache.try_prepare(inputs)); const auto& accepted = cache.get(); Vector result(enthalpy.Size()); result = 3.0; SECTION("Missing point linearizations cannot index outside the accepted batch") { auto incomplete = accepted; incomplete.pop_back(); CHECK_THROWS_AS(boundary::apply_residual_rows(*selected, incomplete, result), std::invalid_argument); CHECK_THROWS_AS(boundary::apply_jacobian_rows(*selected, incomplete, state, result), std::invalid_argument); CHECK_THROWS_AS(boundary::add_jacobian_transpose_rows(*selected, incomplete, enthalpy, result), std::invalid_argument); } SECTION("Short directions cannot index outside an input field") { Vector short_entropy(entropy.Size() - 1); short_entropy = 0.2; const auto direction = composition_state(mesh, enthalpy, short_entropy, composition); CHECK_THROWS_AS(boundary::apply_jacobian_rows(*selected, accepted, direction, result), std::invalid_argument); } SECTION("A nonfinite direction cannot partially replace the rows") { entropy = std::numeric_limits::infinity(); CHECK_THROWS_AS(boundary::apply_jacobian_rows(*selected, accepted, state, result), std::domain_error); } SECTION("Short residual output is rejected before writing") { result.SetSize(enthalpy.Size() - 1); result = 3.0; CHECK_THROWS_AS(boundary::apply_residual_rows(*selected, accepted, result), std::invalid_argument); CHECK_THROWS_AS(boundary::apply_jacobian_rows(*selected, accepted, state, result), std::invalid_argument); CHECK_THROWS_AS(boundary::add_jacobian_transpose_rows(*selected, accepted, enthalpy, result), std::invalid_argument); } for (int index = 0; index < result.Size(); ++index) { CHECK(result[index] == 3.0); } } TEST_CASE("Accepted boundary snapshots retain their identity until a replacement is published", "[surface][unit][boundary]") { fixture::Fixture mesh; test::GeneralEOS model; Vector enthalpy = fixture::sequence(mesh.enthalpy.size(), 1.7, 0.1); Vector entropy = fixture::sequence(mesh.entropy.size(), 0.2, 0.3); Vector composition = fixture::sequence(mesh.composition.size(), 0.1, 0.5); const auto state = composition_state(mesh, enthalpy, entropy, composition); using BoundaryPointer = decltype(boundary::make_pressure_boundary(state)); std::weak_ptr previous_boundary; auto cache = [&]() { const auto original = boundary::make_pressure_boundary(state); previous_boundary = original; const auto inputs = boundary::make_pressure_boundary_inputs(original, eos_input(model), pressure(0.4), state); auto prepared = dependencies::make_computation_cache(boundary::make_pressure_boundary_graph(), inputs); REQUIRE(prepared.try_prepare(inputs)); return prepared; }(); REQUIRE_FALSE(previous_boundary.expired()); const double previous_residual = cache.get()[0].residual; const auto replacement = boundary::make_pressure_boundary(state); const auto inputs = boundary::make_pressure_boundary_inputs(replacement, eos_input(model), pressure(0.4), state); const auto changed = cache.try_prepare(inputs); REQUIRE(changed); CHECK(changed->status>() == Status::computed); CHECK(changed->status>() == Status::computed); CHECK(changed->status>() == Status::computed); CHECK(changed->status() == Status::computed); CHECK(changed->status() == Status::computed); CHECK(cache.get()[0].residual == previous_residual); CHECK(previous_boundary.expired()); }