diff --git a/.github/workflows/ubuntu-sonarcloud.yml b/.github/workflows/ubuntu-sonarcloud.yml index 043b71457b..7721e17e6a 100644 --- a/.github/workflows/ubuntu-sonarcloud.yml +++ b/.github/workflows/ubuntu-sonarcloud.yml @@ -51,7 +51,7 @@ jobs: - name: Run Unit Test run: | build/bin/UnitTests - gcovr --gcov-executable "gcov-${{ matrix.version }}" --root . --filter '^Sources/' --sonarqube build/coverage.xml build + gcovr --gcov-executable "gcov-${{ matrix.version }}" --root . --filter '^(Includes|Sources)/' --sonarqube build/coverage.xml build - name: SonarCloud Scan run: sonar-scanner -Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=utilforever-github env: diff --git a/Includes/API/Python/Particle/MPM/MPMSystemData.hpp b/Includes/API/Python/Particle/MPM/MPMSystemData.hpp new file mode 100644 index 0000000000..5fc8a52d17 --- /dev/null +++ b/Includes/API/Python/Particle/MPM/MPMSystemData.hpp @@ -0,0 +1,19 @@ +// This code is based on Jet framework. +// Copyright (c) 2018 Doyub Kim +// CubbyFlow is voxel-based fluid simulation engine for computer games. +// Copyright (c) 2020 CubbyFlow Team +// Core Part: Chris Ohk, Junwoo Hwang, Jihong Sin, Seungwoo Yoo +// AI Part: Dongheon Cho, Minseo Kim +// We are making my contributions/submissions to this project solely in our +// personal capacity and are not conveying any rights to any intellectual +// property of any third parties. + +#ifndef CUBBYFLOW_PYTHON_MPM_SYSTEM_DATA_HPP +#define CUBBYFLOW_PYTHON_MPM_SYSTEM_DATA_HPP + +#include + +void AddMPMSystemData2(pybind11::module& m); +void AddMPMSystemData3(pybind11::module& m); + +#endif diff --git a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp new file mode 100644 index 0000000000..0577661083 --- /dev/null +++ b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp @@ -0,0 +1,497 @@ +// This code is based on Jet framework. +// Copyright (c) 2018 Doyub Kim +// CubbyFlow is voxel-based fluid simulation engine for computer games. +// Copyright (c) 2020 CubbyFlow Team +// Core Part: Chris Ohk, Junwoo Hwang, Jihong Sin, Seungwoo Yoo +// AI Part: Dongheon Cho, Minseo Kim +// We are making my contributions/submissions to this project solely in our +// personal capacity and are not conveying any rights to any intellectual +// property of any third parties. + +#ifndef CUBBYFLOW_MPM_SYSTEM_DATA_IMPL_HPP +#define CUBBYFLOW_MPM_SYSTEM_DATA_IMPL_HPP + +#include + +#include +#include +#include +#include + +namespace CubbyFlow +{ +template +double CubicBSplineKernel::Weight(double x) +{ + const double ax = std::abs(x); + + if (ax < 1.0) + { + return 0.5 * ax * ax * ax - ax * ax + 2.0 / 3.0; + } + + if (ax < 2.0) + { + const double d = 2.0 - ax; + return d * d * d / 6.0; + } + + return 0.0; +} + +template +double CubicBSplineKernel::Gradient(double x) +{ + const double ax = std::abs(x); + + if (ax < 1.0) + { + return x * (1.5 * ax - 2.0); + } + + if (ax < 2.0) + { + const double d = 2.0 - ax; + return -0.5 * d * d * std::copysign(1.0, x); + } + + return 0.0; +} + +template +void CubicBSplineKernel::GetStencilCoordinates( + const Vector& position, const Vector& gridSpacing, + const Vector& dataOrigin, Vector* normalized, + Vector* firstIndex) +{ + for (size_t axis = 0; axis < N; ++axis) + { + if (!std::isfinite(position[axis]) || + !std::isfinite(gridSpacing[axis]) || + !std::isfinite(dataOrigin[axis]) || gridSpacing[axis] <= 0.0) + { + throw std::invalid_argument("Invalid cubic B-spline input."); + } + + (*normalized)[axis] = + (position[axis] - dataOrigin[axis]) / gridSpacing[axis]; + + const double lowestIndex = std::nextafter( + static_cast(std::numeric_limits::lowest()) + 1.0, + 0.0); + if (const double highestIndex = std::nextafter( + static_cast(std::numeric_limits::max()) - 2.0, + 0.0); + !std::isfinite((*normalized)[axis]) || + (*normalized)[axis] < lowestIndex || + (*normalized)[axis] > highestIndex) + { + throw std::invalid_argument( + "Cubic B-spline index is out of range."); + } + + (*firstIndex)[axis] = + static_cast(std::floor((*normalized)[axis])) - 1; + } +} + +template +CubicBSplineKernel::Entry CubicBSplineKernel::GetStencilEntry( + const Vector& offset, const Vector& normalized, + const Vector& firstIndex, const Vector& gridSpacing) +{ + Entry entry; + std::array axisWeights; + + entry.weight = 1.0; + + for (size_t axis = 0; axis < N; ++axis) + { + entry.index[axis] = + firstIndex[axis] + static_cast(offset[axis]); + axisWeights[axis] = + Weight(normalized[axis] - static_cast(entry.index[axis])); + entry.weight *= axisWeights[axis]; + } + + for (size_t axis = 0; axis < N; ++axis) + { + entry.gradient[axis] = + Gradient(normalized[axis] - + static_cast(entry.index[axis])) / + gridSpacing[axis]; + + for (size_t other = 0; other < N; ++other) + { + if (other != axis) + { + entry.gradient[axis] *= axisWeights[other]; + } + } + } + + return entry; +} + +template +CubicBSplineKernel::Stencil CubicBSplineKernel::GetStencil( + const Vector& position, const Vector& gridSpacing, + const Vector& dataOrigin) +{ + Vector normalized; + Vector firstIndex; + GetStencilCoordinates(position, gridSpacing, dataOrigin, &normalized, + &firstIndex); + + std::array result; + size_t flatIndex = 0; + + ForEachIndex(Vector::MakeConstant(4), + [&result, &flatIndex, &normalized, &firstIndex, + &gridSpacing](auto... rawIndices) { + const Vector offset{ rawIndices... }; + result[flatIndex++] = GetStencilEntry( + offset, normalized, firstIndex, gridSpacing); + }); + + return result; +} + +template +MPMSystemData::MPMSystemData(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin, + size_t numberOfParticles) + : Base{} +{ + ResizeGrid(resolution, gridSpacing, gridOrigin); + Resize(numberOfParticles); +} + +template +void MPMSystemData::Resize(size_t newNumberOfParticles) +{ + Base::Resize(newNumberOfParticles); + + m_particleMasses.Resize(newNumberOfParticles, Base::Mass()); + m_initialVolumes.Resize(newNumberOfParticles, 0.0); + m_deformationStates.Resize(newNumberOfParticles, DeformationState{}); +} + +template +void MPMSystemData::Deserialize(const std::vector& buffer) +{ + Base::Deserialize(buffer); + ResetMPMState(); +} + +template +void MPMSystemData::Set(const ParticleSystemData& other) +{ + Base::Set(other); + ResetMPMState(); +} + +template +void MPMSystemData::ResizeGrid(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin) +{ + ValidateGridParameters(resolution, gridSpacing, gridOrigin); + + m_gridMass.Resize(resolution, gridSpacing, gridOrigin); + m_gridVelocities.Resize(resolution, gridSpacing, gridOrigin); + m_gridVelocitiesBeforeUpdate.Resize(resolution, gridSpacing, gridOrigin); +} + +template +ConstArrayView1 MPMSystemData::ParticleMasses() const +{ + return m_particleMasses.View(); +} + +template +ArrayView1 MPMSystemData::ParticleMasses() +{ + return m_particleMasses.View(); +} + +template +ConstArrayView1 MPMSystemData::InitialVolumes() const +{ + return m_initialVolumes.View(); +} + +template +ArrayView1 MPMSystemData::InitialVolumes() +{ + return m_initialVolumes.View(); +} + +template +ConstArrayView1::DeformationState> +MPMSystemData::DeformationStates() const +{ + return m_deformationStates.View(); +} + +template +ArrayView1::DeformationState> +MPMSystemData::DeformationStates() +{ + return m_deformationStates.View(); +} + +template +const VertexCenteredScalarGrid& MPMSystemData::GridMass() const +{ + return m_gridMass; +} + +template +VertexCenteredScalarGrid& MPMSystemData::GridMass() +{ + return m_gridMass; +} + +template +const VertexCenteredVectorGrid& MPMSystemData::GridVelocities() const +{ + return m_gridVelocities; +} + +template +VertexCenteredVectorGrid& MPMSystemData::GridVelocities() +{ + return m_gridVelocities; +} + +template +const VertexCenteredVectorGrid& +MPMSystemData::GridVelocitiesBeforeUpdate() const +{ + return m_gridVelocitiesBeforeUpdate; +} + +template +VertexCenteredVectorGrid& MPMSystemData::GridVelocitiesBeforeUpdate() +{ + return m_gridVelocitiesBeforeUpdate; +} + +template +double MPMSystemData::FLIPBlendingFactor() const +{ + return m_flipBlendingFactor; +} + +template +void MPMSystemData::SetFLIPBlendingFactor(double factor) +{ + if (!std::isfinite(factor) || factor < 0.0 || factor > 1.0) + { + throw std::invalid_argument("FLIP blending factor must be in [0, 1]."); + } + + m_flipBlendingFactor = factor; +} + +template +void MPMSystemData::TransferFromParticlesToGrid() +{ + ValidateGridState(); + + const auto positions = this->Positions(); + const auto velocities = this->Velocities(); + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + if (!std::isfinite(m_particleMasses[i]) || m_particleMasses[i] <= 0.0 || + !IsFinite(positions[i]) || !IsFinite(velocities[i])) + { + throw std::invalid_argument("Invalid MPM particle state."); + } + } + + m_gridMass.Fill(0.0, ExecutionPolicy::Serial); + m_gridVelocities.Fill(Vector{}, ExecutionPolicy::Serial); + + const auto dataSize = m_gridMass.DataSize(); + const auto gridSpacing = m_gridMass.GridSpacing(); + const auto dataOrigin = m_gridMass.DataOrigin(); + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + const auto stencil = CubicBSplineKernel::GetStencil( + positions[i], gridSpacing, dataOrigin); + + for (const auto& entry : stencil) + { + if (entry.weight == 0.0) + { + continue; + } + + const auto index = ClampIndex(entry.index, dataSize); + const double mass = entry.weight * m_particleMasses[i]; + + m_gridMass(index) += mass; + m_gridVelocities(index) += mass * velocities[i]; + } + } + + m_gridMass.ForEachDataPointIndex([this](const Vector& index) { + const double mass = m_gridMass(index); + if (mass > 0.0) + { + m_gridVelocities(index) /= mass; + } + }); + + m_gridVelocitiesBeforeUpdate.Set(m_gridVelocities); +} + +template +void MPMSystemData::TransferFromGridToParticles() +{ + ValidateGridState(); + + const auto positions = this->Positions(); + auto velocities = this->Velocities(); + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + if (!IsFinite(positions[i]) || !IsFinite(velocities[i])) + { + throw std::invalid_argument("Invalid MPM particle state."); + } + } + + m_gridVelocities.ForEachDataPointIndex( + [this](const Vector& index) { + if (!IsFinite(m_gridVelocities(index)) || + !IsFinite(m_gridVelocitiesBeforeUpdate(index))) + { + throw std::invalid_argument("Invalid MPM grid velocity."); + } + }); + + const auto dataSize = m_gridVelocities.DataSize(); + const auto gridSpacing = m_gridVelocities.GridSpacing(); + const auto dataOrigin = m_gridVelocities.DataOrigin(); + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + Vector picVelocity; + Vector flipDelta; + const auto stencil = CubicBSplineKernel::GetStencil( + positions[i], gridSpacing, dataOrigin); + + for (const auto& entry : stencil) + { + if (entry.weight == 0.0) + { + continue; + } + + const auto index = ClampIndex(entry.index, dataSize); + picVelocity += entry.weight * m_gridVelocities(index); + flipDelta += entry.weight * (m_gridVelocities(index) - + m_gridVelocitiesBeforeUpdate(index)); + } + + const Vector flipVelocity = velocities[i] + flipDelta; + const Vector result = + (1.0 - m_flipBlendingFactor) * picVelocity + + m_flipBlendingFactor * flipVelocity; + + velocities[i] = result; + } +} + +template +Vector MPMSystemData::ClampIndex( + const Vector& index, const Vector& dataSize) +{ + Vector result; + + for (size_t axis = 0; axis < N; ++axis) + { + result[axis] = index[axis] < 0 + ? 0 + : std::min(static_cast(index[axis]), + dataSize[axis] - 1); + } + + return result; +} + +template +bool MPMSystemData::IsFinite(const Vector& value) +{ + for (size_t axis = 0; axis < N; ++axis) + { + if (!std::isfinite(value[axis])) + { + return false; + } + } + + return true; +} + +template +void MPMSystemData::ValidateGridParameters( + const Vector& resolution, const Vector& gridSpacing, + const Vector& gridOrigin) +{ + for (size_t axis = 0; axis < N; ++axis) + { + if (resolution[axis] == 0 || + resolution[axis] == std::numeric_limits::max() || + !std::isfinite(gridSpacing[axis]) || gridSpacing[axis] <= 0.0 || + !std::isfinite(gridOrigin[axis])) + { + throw std::invalid_argument("Invalid MPM grid parameters."); + } + } +} + +template +void MPMSystemData::ValidateGridState() const +{ + ValidateGridParameters(m_gridMass.Resolution(), m_gridMass.GridSpacing(), + m_gridMass.Origin()); + ValidateGridParameters(m_gridVelocities.Resolution(), + m_gridVelocities.GridSpacing(), + m_gridVelocities.Origin()); + ValidateGridParameters(m_gridVelocitiesBeforeUpdate.Resolution(), + m_gridVelocitiesBeforeUpdate.GridSpacing(), + m_gridVelocitiesBeforeUpdate.Origin()); + + if (m_gridMass.Resolution() != m_gridVelocities.Resolution() || + m_gridMass.Resolution() != m_gridVelocitiesBeforeUpdate.Resolution() || + m_gridMass.GridSpacing() != m_gridVelocities.GridSpacing() || + m_gridMass.GridSpacing() != + m_gridVelocitiesBeforeUpdate.GridSpacing() || + m_gridMass.Origin() != m_gridVelocities.Origin() || + m_gridMass.Origin() != m_gridVelocitiesBeforeUpdate.Origin()) + { + throw std::invalid_argument("MPM grids must have matching geometry."); + } +} + +template +void MPMSystemData::ResetMPMState() +{ + m_particleMasses.Fill(Base::Mass()); + m_initialVolumes.Fill(0.0); + m_deformationStates.Fill(DeformationState{}); + m_gridMass.Fill(0.0, ExecutionPolicy::Serial); + m_gridVelocities.Fill(Vector{}, ExecutionPolicy::Serial); + m_gridVelocitiesBeforeUpdate.Fill(Vector{}, + ExecutionPolicy::Serial); +} +} // namespace CubbyFlow + +#endif diff --git a/Includes/Core/Particle/MPM/MPMSystemData.hpp b/Includes/Core/Particle/MPM/MPMSystemData.hpp new file mode 100644 index 0000000000..008c6ca6bf --- /dev/null +++ b/Includes/Core/Particle/MPM/MPMSystemData.hpp @@ -0,0 +1,191 @@ +// This code is based on Jet framework. +// Copyright (c) 2018 Doyub Kim +// CubbyFlow is voxel-based fluid simulation engine for computer games. +// Copyright (c) 2020 CubbyFlow Team +// Core Part: Chris Ohk, Junwoo Hwang, Jihong Sin, Seungwoo Yoo +// AI Part: Dongheon Cho, Minseo Kim +// We are making my contributions/submissions to this project solely in our +// personal capacity and are not conveying any rights to any intellectual +// property of any third parties. + +#ifndef CUBBYFLOW_MPM_SYSTEM_DATA_HPP +#define CUBBYFLOW_MPM_SYSTEM_DATA_HPP + +#include +#include +#include +#include + +#include + +namespace CubbyFlow +{ +//! +//! \brief Tensor-product cubic B-spline interpolation kernel. +//! +template +class CubicBSplineKernel final +{ + public: + static_assert(N == 2 || N == 3, "MPM supports only 2-D and 3-D."); + + static constexpr size_t STENCIL_SIZE = N == 2 ? 16 : 64; + + struct Entry + { + Vector index; + double weight = 0.0; + Vector gradient; + }; + + using Stencil = std::array; + + [[nodiscard]] static double Weight(double x); + + [[nodiscard]] static double Gradient(double x); + + [[nodiscard]] static Stencil GetStencil( + const Vector& position, const Vector& gridSpacing, + const Vector& dataOrigin); + + private: + static void GetStencilCoordinates(const Vector& position, + const Vector& gridSpacing, + const Vector& dataOrigin, + Vector* normalized, + Vector* firstIndex); + + [[nodiscard]] static Entry GetStencilEntry( + const Vector& offset, const Vector& normalized, + const Vector& firstIndex, + const Vector& gridSpacing); +}; + +//! +//! \brief N-D material point method particle and grid state. +//! +//! \note Serialization preserves only the inherited particle-system state; +//! MPM-specific particle and grid state is reset after deserialization. +//! +template +class MPMSystemData final : public ParticleSystemData +{ + public: + static_assert(N == 2 || N == 3, "MPM supports only 2-D and 3-D."); + + using Base = ParticleSystemData; + using DeformationState = SnowDeformationState; + + //! Constructs MPM state with a vertex-centered background grid. + explicit MPMSystemData( + const Vector& resolution = + Vector::MakeConstant(1), + const Vector& gridSpacing = + Vector::MakeConstant(1.0), + const Vector& gridOrigin = Vector{}, + size_t numberOfParticles = 0); + + //! Resizes particle state, initializing new MPM attributes. + void Resize(size_t newNumberOfParticles) override; + + //! Deserializes inherited particle state and resets MPM-specific state. + void Deserialize(const std::vector& buffer) override; + + //! Copies inherited particle state and resets MPM-specific state. + void Set(const ParticleSystemData& other) override; + + //! Resizes the background grid without changing particle state. + void ResizeGrid(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin); + + //! Returns per-particle masses. + [[nodiscard]] ConstArrayView1 ParticleMasses() const; + + //! Returns per-particle masses. + [[nodiscard]] ArrayView1 ParticleMasses(); + + //! Returns per-particle initial volumes. + [[nodiscard]] ConstArrayView1 InitialVolumes() const; + + //! Returns per-particle initial volumes. + [[nodiscard]] ArrayView1 InitialVolumes(); + + //! Returns per-particle deformation states. + [[nodiscard]] ConstArrayView1 DeformationStates() const; + + //! Returns per-particle deformation states. + [[nodiscard]] ArrayView1 DeformationStates(); + + //! Returns grid mass. + [[nodiscard]] const VertexCenteredScalarGrid& GridMass() const; + + //! Returns grid mass. + [[nodiscard]] VertexCenteredScalarGrid& GridMass(); + + //! Returns current grid velocities. + [[nodiscard]] const VertexCenteredVectorGrid& GridVelocities() const; + + //! Returns current grid velocities. + [[nodiscard]] VertexCenteredVectorGrid& GridVelocities(); + + //! Returns grid velocities before the grid update. + [[nodiscard]] const VertexCenteredVectorGrid& + GridVelocitiesBeforeUpdate() const; + + //! Returns grid velocities before the grid update. + [[nodiscard]] VertexCenteredVectorGrid& GridVelocitiesBeforeUpdate(); + + //! Returns the FLIP fraction used for grid-to-particle transfer. + [[nodiscard]] double FLIPBlendingFactor() const; + + //! Sets the FLIP fraction used for grid-to-particle transfer. + void SetFLIPBlendingFactor(double factor); + + //! Transfers particle mass and momentum to the background grid. + //! Stencil nodes outside the finite grid are clamped to its boundary. + void TransferFromParticlesToGrid(); + + //! Transfers updated background-grid velocities to particles. + //! Stencil nodes outside the finite grid are clamped to its boundary. + void TransferFromGridToParticles(); + + private: + [[nodiscard]] static Vector ClampIndex( + const Vector& index, const Vector& dataSize); + + [[nodiscard]] static bool IsFinite(const Vector& value); + + static void ValidateGridParameters(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin); + + void ValidateGridState() const; + + void ResetMPMState(); + + Array1 m_particleMasses; + Array1 m_initialVolumes; + Array1 m_deformationStates; + VertexCenteredScalarGrid m_gridMass; + VertexCenteredVectorGrid m_gridVelocities; + VertexCenteredVectorGrid m_gridVelocitiesBeforeUpdate; + double m_flipBlendingFactor = 0.95; +}; + +//! 2-D material point method system data. +using MPMSystemData2 = MPMSystemData<2>; + +//! 3-D material point method system data. +using MPMSystemData3 = MPMSystemData<3>; + +//! Shared pointer type of MPMSystemData2. +using MPMSystemData2Ptr = std::shared_ptr; + +//! Shared pointer type of MPMSystemData3. +using MPMSystemData3Ptr = std::shared_ptr; +} // namespace CubbyFlow + +#include + +#endif diff --git a/Includes/Core/Particle/ParticleSystemData.hpp b/Includes/Core/Particle/ParticleSystemData.hpp index b4cf5a253a..8f05c2f674 100644 --- a/Includes/Core/Particle/ParticleSystemData.hpp +++ b/Includes/Core/Particle/ParticleSystemData.hpp @@ -86,7 +86,7 @@ class ParticleSystemData : public Serializable //! //! \param[in] newNumberOfParticles New number of particles. //! - void Resize(size_t newNumberOfParticles); + virtual void Resize(size_t newNumberOfParticles); //! Returns the number of particles. [[nodiscard]] size_t NumberOfParticles() const; @@ -231,7 +231,7 @@ class ParticleSystemData : public Serializable void Deserialize(const std::vector& buffer) override; //! Copies from other particle system data. - void Set(const ParticleSystemData& other); + virtual void Set(const ParticleSystemData& other); protected: template @@ -284,4 +284,4 @@ using ParticleSystemData2Ptr = std::shared_ptr; using ParticleSystemData3Ptr = std::shared_ptr; } // namespace CubbyFlow -#endif \ No newline at end of file +#endif diff --git a/Includes/Core/Particle/SPHSystemData.hpp b/Includes/Core/Particle/SPHSystemData.hpp index 737f7772c1..1e7e103b17 100644 --- a/Includes/Core/Particle/SPHSystemData.hpp +++ b/Includes/Core/Particle/SPHSystemData.hpp @@ -35,6 +35,7 @@ class SPHSystemData : public ParticleSystemData using Base::Positions; using Base::ScalarDataAt; using Base::Serialize; + using Base::Set; //! Constructs empty SPH system. SPHSystemData(); @@ -256,4 +257,4 @@ using SPHSystemData2Ptr = std::shared_ptr; using SPHSystemData3Ptr = std::shared_ptr; } // namespace CubbyFlow -#endif \ No newline at end of file +#endif diff --git a/Sources/API/Python/Particle/MPM/MPMSystemData.cpp b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp new file mode 100644 index 0000000000..f50e63444d --- /dev/null +++ b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp @@ -0,0 +1,145 @@ +// This code is based on Jet framework. +// Copyright (c) 2018 Doyub Kim +// CubbyFlow is voxel-based fluid simulation engine for computer games. +// Copyright (c) 2020 CubbyFlow Team +// Core Part: Chris Ohk, Junwoo Hwang, Jihong Sin, Seungwoo Yoo +// AI Part: Dongheon Cho, Minseo Kim +// We are making my contributions/submissions to this project solely in our +// personal capacity and are not conveying any rights to any intellectual +// property of any third parties. + +#include +#include + +#include + +using namespace CubbyFlow; + +namespace +{ +namespace py = pybind11; + +template +using MPMClass = py::class_, std::shared_ptr>, + ParticleSystemData>; + +template +py::array_t MatrixView(SnowDeformationState& state, + Matrix& matrix) +{ + return py::array_t( + { static_cast(N), static_cast(N) }, + { static_cast(sizeof(double) * N), + static_cast(sizeof(double)) }, + matrix.data(), py::cast(&state)); +} + +template +void AddSnowDeformationState(py::module& m, const char* name) +{ + using State = SnowDeformationState; + py::class_(m, name) + .def(py::init<>()) + .def_property_readonly( + "elastic", + [](State& state) { return MatrixView(state, state.elastic); }) + .def_property_readonly("plastic", [](State& state) { + return MatrixView(state, state.plastic); + }); +} + +template +py::array_t ScalarView(MPMSystemData& instance, + ArrayView1 view) +{ + return py::array_t({ static_cast(view.Length()) }, + { static_cast(sizeof(double)) }, + view.data(), py::cast(&instance)); +} + +template +py::list DeformationStates(MPMSystemData& instance) +{ + py::list result; + auto parent = py::cast(&instance, py::return_value_policy::reference); + for (auto& state : instance.DeformationStates()) + { + result.append( + py::cast(&state, py::return_value_policy::reference, parent)); + } + return result; +} + +template +void BindParticleState(MPMClass& cls) +{ + cls.def_property_readonly("particleMasses", + [](MPMSystemData& instance) { + return ScalarView(instance, + instance.ParticleMasses()); + }) + .def_property_readonly("initialVolumes", + [](MPMSystemData& instance) { + return ScalarView(instance, + instance.InitialVolumes()); + }) + .def_property_readonly("deformationStates", &DeformationStates); +} + +template +void BindGridState(MPMClass& cls) +{ + cls.def("ResizeGrid", &MPMSystemData::ResizeGrid) + .def_property_readonly( + "gridMass", + [](MPMSystemData& instance) -> auto& { + return instance.GridMass(); + }, + py::return_value_policy::reference_internal) + .def_property_readonly( + "gridVelocities", + [](MPMSystemData& instance) -> auto& { + return instance.GridVelocities(); + }, + py::return_value_policy::reference_internal) + .def_property_readonly( + "gridVelocitiesBeforeUpdate", + [](MPMSystemData& instance) -> auto& { + return instance.GridVelocitiesBeforeUpdate(); + }, + py::return_value_policy::reference_internal) + .def_property("flipBlendingFactor", + &MPMSystemData::FLIPBlendingFactor, + &MPMSystemData::SetFLIPBlendingFactor) + .def("TransferFromParticlesToGrid", + &MPMSystemData::TransferFromParticlesToGrid) + .def("TransferFromGridToParticles", + &MPMSystemData::TransferFromGridToParticles); +} + +template +void AddMPMSystemData(py::module& m, const char* className, + const char* stateName) +{ + AddSnowDeformationState(m, stateName); + MPMClass cls(m, className); + cls.def(py::init&, const Vector&, + const Vector&, size_t>(), + py::arg("resolution") = Vector::MakeConstant(1), + py::arg("gridSpacing") = Vector::MakeConstant(1.0), + py::arg("gridOrigin") = Vector{}, + py::arg("numberOfParticles") = 0); + BindParticleState(cls); + BindGridState(cls); +} +} // namespace + +void AddMPMSystemData2(pybind11::module& m) +{ + AddMPMSystemData<2>(m, "MPMSystemData2", "SnowDeformationState2"); +} + +void AddMPMSystemData3(pybind11::module& m) +{ + AddMPMSystemData<3>(m, "MPMSystemData3", "SnowDeformationState3"); +} diff --git a/Sources/API/Python/Particle/SPH/SPHSystemData.cpp b/Sources/API/Python/Particle/SPH/SPHSystemData.cpp index 8925eb78bd..6cbb7cfebc 100644 --- a/Sources/API/Python/Particle/SPH/SPHSystemData.cpp +++ b/Sources/API/Python/Particle/SPH/SPHSystemData.cpp @@ -89,7 +89,9 @@ void AddSPHSystemData2(pybind11::module& m) R"pbdoc( Builds neighbor lists with kernel radius. )pbdoc") - .def("Set", &SPHSystemData2::Set, + .def("Set", + static_cast( + &SPHSystemData2::Set), R"pbdoc( Copies from other SPH system data. )pbdoc"); @@ -169,8 +171,10 @@ void AddSPHSystemData3(pybind11::module& m) R"pbdoc( Builds neighbor lists with kernel radius. )pbdoc") - .def("Set", &SPHSystemData3::Set, + .def("Set", + static_cast( + &SPHSystemData3::Set), R"pbdoc( Copies from other SPH system data. )pbdoc"); -} \ No newline at end of file +} diff --git a/Sources/API/Python/main.cpp b/Sources/API/Python/main.cpp index b0e300d285..cce67f6902 100644 --- a/Sources/API/Python/main.cpp +++ b/Sources/API/Python/main.cpp @@ -54,6 +54,7 @@ #include #include #include +#include #include #include #include @@ -218,6 +219,8 @@ PYBIND11_MODULE(pyCubbyFlow, m) AddGridSystemData3(m); AddParticleSystemData2(m); AddParticleSystemData3(m); + AddMPMSystemData2(m); + AddMPMSystemData3(m); AddSPHSystemData2(m); AddSPHSystemData3(m); @@ -338,4 +341,4 @@ PYBIND11_MODULE(pyCubbyFlow, m) #else m.attr("__version__") = pybind11::str("Dev"); #endif -} \ No newline at end of file +} diff --git a/Sources/Core/Particle/ParticleSystemData.cpp b/Sources/Core/Particle/ParticleSystemData.cpp index b8a5aad118..1014895c6b 100644 --- a/Sources/Core/Particle/ParticleSystemData.cpp +++ b/Sources/Core/Particle/ParticleSystemData.cpp @@ -72,7 +72,7 @@ ParticleSystemData::ParticleSystemData(size_t numberOfParticles) Vector::MakeConstant(DEFAULT_HASH_GRID_RESOLUTION), 2.0 * m_radius); - Resize(numberOfParticles); + ParticleSystemData::Resize(numberOfParticles); } template @@ -422,12 +422,14 @@ void ParticleSystemData::Deserialize(const std::vector& buffer) GetFlatbuffersParticleSystemData::GetParticleSystemData( buffer.data()); Deserialize(fbsParticleSystemData, *this); + Resize(NumberOfParticles()); } template void ParticleSystemData::Set(const ParticleSystemData& other) { *this = other; + Resize(NumberOfParticles()); } template diff --git a/Tests/PythonTests/test_mpm_system_data.py b/Tests/PythonTests/test_mpm_system_data.py new file mode 100644 index 0000000000..f6c391fff7 --- /dev/null +++ b/Tests/PythonTests/test_mpm_system_data.py @@ -0,0 +1,65 @@ +import numpy as np +import pytest + +import pyCubbyFlow + + +@pytest.mark.parametrize( + "data_type,resolution,spacing,origin,grid_velocity", + [ + ( + pyCubbyFlow.MPMSystemData2, + pyCubbyFlow.Vector2UZ(3, 3), + pyCubbyFlow.Vector2D(1.0, 1.0), + pyCubbyFlow.Vector2D(), + (1.0, 1.0), + ), + ( + pyCubbyFlow.MPMSystemData3, + pyCubbyFlow.Vector3UZ(3, 3, 3), + pyCubbyFlow.Vector3D(1.0, 1.0, 1.0), + pyCubbyFlow.Vector3D(), + (1.0, 1.0, 1.0), + ), + ], +) +def test_mpm_system_data_api( + data_type, resolution, spacing, origin, grid_velocity +): + data = data_type(resolution, spacing, origin, 1) + + masses = np.asarray(data.particleMasses) + volumes = np.asarray(data.initialVolumes) + assert masses.shape == (1,) + assert volumes.shape == (1,) + masses[0] = 2.0 + volumes[0] = 3.0 + assert data.particleMasses[0] == 2.0 + assert data.initialVolumes[0] == 3.0 + + states = data.deformationStates + assert len(states) == 1 + states[0].elastic[0, 0] = 2.0 + states[0].plastic[0, 0] = 3.0 + assert states[0].elastic[0, 0] == 2.0 + assert states[0].plastic[0, 0] == 3.0 + + assert data.gridMass.resolution == resolution + assert data.gridVelocities.resolution == resolution + assert data.gridVelocitiesBeforeUpdate.resolution == resolution + + data.flipBlendingFactor = 0.5 + assert data.flipBlendingFactor == 0.5 + data.TransferFromParticlesToGrid() + data.gridVelocities.Fill(grid_velocity) + data.TransferFromGridToParticles() + resized_grid = resolution + resolution, spacing + spacing, origin + spacing + data.ResizeGrid(*resized_grid) + for grid in ( + data.gridMass, + data.gridVelocities, + data.gridVelocitiesBeforeUpdate, + ): + assert grid.resolution == resized_grid[0] + assert grid.gridSpacing == resized_grid[1] + assert grid.gridOrigin == resized_grid[2] diff --git a/Tests/UnitTests/MPMSystemDataTests.cpp b/Tests/UnitTests/MPMSystemDataTests.cpp new file mode 100644 index 0000000000..490e9800bb --- /dev/null +++ b/Tests/UnitTests/MPMSystemDataTests.cpp @@ -0,0 +1,426 @@ +// This code is based on Jet framework. +// Copyright (c) 2018 Doyub Kim +// CubbyFlow is voxel-based fluid simulation engine for computer games. +// Copyright (c) 2020 CubbyFlow Team +// Core Part: Chris Ohk, Junwoo Hwang, Jihong Sin, Seungwoo Yoo +// AI Part: Dongheon Cho, Minseo Kim +// We are making my contributions/submissions to this project solely in our +// personal capacity and are not conveying any rights to any intellectual +// property of any third parties. + +#include "gtest/gtest.h" + +#include + +#include + +using namespace CubbyFlow; + +namespace +{ +template +void DirtyMPMState(MPMSystemData& data) +{ + data.ParticleMasses()[0] = 7.0; + data.InitialVolumes()[0] = 8.0; + data.DeformationStates()[0].elastic(0, 0) = 9.0; + data.GridMass().Fill(10.0, ExecutionPolicy::Serial); + data.GridVelocities().Fill(Vector::MakeConstant(11.0), + ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Fill( + Vector::MakeConstant(12.0), ExecutionPolicy::Serial); +} + +template +void ExpectMPMStateReset(const MPMSystemData& data) +{ + const Vector zeroIndex{}; + const Vector zero{}; + const auto identity = Matrix::MakeIdentity(); + + EXPECT_DOUBLE_EQ(data.ParticleMasses()[0], data.Mass()); + EXPECT_DOUBLE_EQ(data.InitialVolumes()[0], 0.0); + EXPECT_TRUE(data.DeformationStates()[0].elastic.IsSimilar(identity)); + EXPECT_DOUBLE_EQ(data.GridMass()(zeroIndex), 0.0); + EXPECT_TRUE(data.GridVelocities()(zeroIndex).IsSimilar(zero)); + EXPECT_TRUE(data.GridVelocitiesBeforeUpdate()(zeroIndex).IsSimilar(zero)); +} + +template +void ExpectParticleStateResizes() +{ + MPMSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + + data.ParticleMasses()[0] = 1.25; + data.InitialVolumes()[0] = 0.5; + data.SetMass(2.5); + data.AddParticle(Vector::MakeConstant(0.25)); + ASSERT_EQ(data.NumberOfParticles(), 2u); + EXPECT_DOUBLE_EQ(data.ParticleMasses()[0], 1.25); + EXPECT_DOUBLE_EQ(data.ParticleMasses()[1], 2.5); + EXPECT_DOUBLE_EQ(data.InitialVolumes()[0], 0.5); + EXPECT_DOUBLE_EQ(data.InitialVolumes()[1], 0.0); + + const auto identity = Matrix::MakeIdentity(); + EXPECT_TRUE(data.DeformationStates()[0].elastic.IsSimilar(identity)); + EXPECT_TRUE(data.DeformationStates()[1].elastic.IsSimilar(identity)); + EXPECT_TRUE(data.DeformationStates()[0].plastic.IsSimilar(identity)); + EXPECT_TRUE(data.DeformationStates()[1].plastic.IsSimilar(identity)); +} + +template +void ExpectBaseSetResizesMPMState() +{ + MPMSystemData data{ Vector::MakeConstant(2), + Vector::MakeConstant(1.0), + {}, + 1 }; + ParticleSystemData source{ 3 }; + source.SetMass(4.0); + DirtyMPMState(data); + + data.Set(source); + + EXPECT_EQ(data.NumberOfParticles(), 3u); + EXPECT_EQ(data.ParticleMasses().Length(), 3u); + EXPECT_EQ(data.InitialVolumes().Length(), 3u); + EXPECT_EQ(data.DeformationStates().Length(), 3u); + ExpectMPMStateReset(data); +} + +template +void ExpectBaseDeserializeResizesMPMState() +{ + ParticleSystemData source{ 3 }; + source.SetMass(4.0); + std::vector buffer; + source.Serialize(&buffer); + + MPMSystemData data{ Vector::MakeConstant(2), + Vector::MakeConstant(1.0), + {}, + 1 }; + DirtyMPMState(data); + data.Deserialize(buffer); + + EXPECT_EQ(data.NumberOfParticles(), 3u); + EXPECT_EQ(data.ParticleMasses().Length(), 3u); + EXPECT_EQ(data.InitialVolumes().Length(), 3u); + EXPECT_EQ(data.DeformationStates().Length(), 3u); + ExpectMPMStateReset(data); +} + +template +void ExpectGridStateResizes() +{ + MPMSystemData data; + const auto resolution = Vector::MakeConstant(3); + const auto spacing = Vector::MakeConstant(0.5); + const auto origin = Vector::MakeConstant(-1.0); + + data.ResizeGrid(resolution, spacing, origin); + + for (size_t axis = 0; axis < N; ++axis) + { + EXPECT_EQ(data.GridMass().Resolution()[axis], resolution[axis]); + EXPECT_EQ(data.GridVelocities().Resolution()[axis], resolution[axis]); + EXPECT_EQ(data.GridVelocitiesBeforeUpdate().Resolution()[axis], + resolution[axis]); + EXPECT_DOUBLE_EQ(data.GridMass().GridSpacing()[axis], spacing[axis]); + EXPECT_DOUBLE_EQ(data.GridMass().Origin()[axis], origin[axis]); + } +} + +template +void ExpectStencilPartitionAndGradient() +{ + const Vector spacing = Vector::MakeConstant(0.5); + const Vector position = Vector::MakeConstant(1.125); + const auto stencil = CubicBSplineKernel::GetStencil(position, spacing, + Vector{}); + + double weightSum = 0.0; + Vector gradientSum; + + for (const auto& entry : stencil) + { + weightSum += entry.weight; + gradientSum += entry.gradient; + } + + EXPECT_NEAR(weightSum, 1.0, 1e-12); + EXPECT_NEAR(gradientSum.Length(), 0.0, 1e-12); + + constexpr double epsilon = 1e-6; + auto shifted = position; + + shifted[0] += epsilon; + + const auto shiftedStencil = CubicBSplineKernel::GetStencil( + shifted, spacing, Vector{}); + + for (size_t i = 0; i < stencil.size(); ++i) + { + EXPECT_NEAR((shiftedStencil[i].weight - stencil[i].weight) / epsilon, + stencil[i].gradient[0], 1e-5); + } +} + +template +void ExpectStencilRejectsUnrepresentableCoordinates() +{ + const auto zero = Vector{}; + const auto unit = Vector::MakeConstant(1.0); + const auto largest = + Vector::MakeConstant(std::numeric_limits::max()); + const auto lowest = + Vector::MakeConstant(std::numeric_limits::lowest()); + + EXPECT_THROW((void)CubicBSplineKernel::GetStencil(unit, zero, {}), + std::invalid_argument); + EXPECT_THROW((void)CubicBSplineKernel::GetStencil(largest, unit, {}), + std::invalid_argument); + EXPECT_THROW((void)CubicBSplineKernel::GetStencil(largest, unit, lowest), + std::invalid_argument); +} + +template +void ExpectParticleToGridConservation(const Vector& firstPosition) +{ + MPMSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 2 }; + data.Positions()[0] = firstPosition; + data.Positions()[1] = Vector::MakeConstant(2.25); + data.Velocities()[0] = Vector::MakeConstant(2.0); + data.Velocities()[1] = Vector::MakeConstant(-1.0); + data.ParticleMasses()[0] = 2.0; + data.ParticleMasses()[1] = 3.0; + + data.TransferFromParticlesToGrid(); + + double gridMass = 0.0; + Vector gridMomentum; + + data.GridMass().ForEachDataPointIndex( + [&data, &gridMass, &gridMomentum](const Vector& index) { + const double mass = data.GridMass()(index); + gridMass += mass; + gridMomentum += mass * data.GridVelocities()(index); + EXPECT_TRUE(data.GridVelocities()(index).IsSimilar( + data.GridVelocitiesBeforeUpdate()(index), 1e-12)); + }); + + EXPECT_NEAR(gridMass, 5.0, 1e-11); + EXPECT_TRUE( + gridMomentum.IsSimilar(Vector::MakeConstant(1.0), 1e-11)); +} + +template +void ExpectGridToParticleBlend(double factor, double expected, + const Vector& position, + bool useDefault = false) +{ + MPMSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + data.Positions()[0] = position; + data.Velocities()[0] = Vector::MakeConstant(10.0); + + data.GridVelocitiesBeforeUpdate().Fill(Vector::MakeConstant(1.0), + ExecutionPolicy::Serial); + data.GridVelocities().Fill(Vector::MakeConstant(3.0), + ExecutionPolicy::Serial); + + if (!useDefault) + { + data.SetFLIPBlendingFactor(factor); + } + + data.TransferFromGridToParticles(); + + EXPECT_TRUE(data.Velocities()[0].IsSimilar( + Vector::MakeConstant(expected), 1e-12)); +} + +template +void ExpectRejectsDivergentGridState() +{ + MPMSystemData data{ Vector::MakeConstant(4) }; + data.GridMass().Clear(); + EXPECT_THROW(data.TransferFromParticlesToGrid(), std::invalid_argument); + + data.ResizeGrid(Vector::MakeConstant(4), + Vector::MakeConstant(1.0), {}); + data.GridVelocitiesBeforeUpdate().Resize( + Vector::MakeConstant(2), + Vector::MakeConstant(1.0), {}); + EXPECT_THROW(data.TransferFromGridToParticles(), std::invalid_argument); +} + +template +void ExpectInvalidGridLeavesParticleVelocitiesUnchanged() +{ + MPMSystemData data{ Vector::MakeConstant(8), + Vector::MakeConstant(1.0), + {}, + 2 }; + data.Positions()[0] = Vector::MakeConstant(1.0); + data.Positions()[1] = Vector::MakeConstant(7.0); + data.Velocities()[0] = Vector::MakeConstant(10.0); + data.Velocities()[1] = Vector::MakeConstant(20.0); + data.GridVelocitiesBeforeUpdate().Fill(Vector::MakeConstant(1.0), + ExecutionPolicy::Serial); + data.GridVelocities().Fill(Vector::MakeConstant(2.0), + ExecutionPolicy::Serial); + + auto invalidVelocity = Vector{}; + invalidVelocity[0] = std::numeric_limits::infinity(); + data.GridVelocities()(Vector::MakeConstant(7)) = invalidVelocity; + + EXPECT_THROW(data.TransferFromGridToParticles(), std::invalid_argument); + EXPECT_TRUE(data.Velocities()[0].IsSimilar( + Vector::MakeConstant(10.0), 1e-12)); + EXPECT_TRUE(data.Velocities()[1].IsSimilar( + Vector::MakeConstant(20.0), 1e-12)); +} + +template +void ExpectRejectsInvalidInput() +{ + MPMSystemData data{ Vector::MakeConstant(2) }; + const Vector zeroResolution{}; + const auto unitSpacing = Vector::MakeConstant(1.0); + auto negativeSpacing = unitSpacing; + negativeSpacing[0] = -1.0; + const auto overflowingResolution = + Vector::MakeConstant(std::numeric_limits::max()); + + EXPECT_THROW(data.ResizeGrid(zeroResolution, unitSpacing, {}), + std::invalid_argument); + EXPECT_THROW(data.ResizeGrid(Vector::MakeConstant(2), + negativeSpacing, {}), + std::invalid_argument); + EXPECT_THROW(data.ResizeGrid(overflowingResolution, unitSpacing, {}), + std::invalid_argument); + EXPECT_THROW(data.SetFLIPBlendingFactor(-0.1), std::invalid_argument); + EXPECT_THROW(data.SetFLIPBlendingFactor(1.1), std::invalid_argument); + EXPECT_THROW( + data.SetFLIPBlendingFactor(std::numeric_limits::quiet_NaN()), + std::invalid_argument); + + data.Resize(1); + data.ParticleMasses()[0] = 0.0; + EXPECT_THROW(data.TransferFromParticlesToGrid(), std::invalid_argument); + data.ParticleMasses()[0] = 1.0; + data.Positions()[0][0] = std::numeric_limits::infinity(); + EXPECT_THROW(data.TransferFromParticlesToGrid(), std::invalid_argument); + EXPECT_THROW(data.TransferFromGridToParticles(), std::invalid_argument); + + data.Positions()[0] = Vector{}; + data.Velocities()[0] = Vector{}; + auto invalidGridVelocity = Vector{}; + invalidGridVelocity[0] = std::numeric_limits::infinity(); + data.GridVelocities().Fill(invalidGridVelocity, ExecutionPolicy::Serial); + EXPECT_THROW(data.TransferFromGridToParticles(), std::invalid_argument); +} +} // namespace + +TEST(CubicBSplineKernel, Values) +{ + EXPECT_NEAR(CubicBSplineKernel<2>::Weight(0.0), 2.0 / 3.0, 1e-12); + EXPECT_NEAR(CubicBSplineKernel<2>::Weight(1.0), 1.0 / 6.0, 1e-12); + EXPECT_DOUBLE_EQ(CubicBSplineKernel<2>::Weight(2.0), 0.0); + EXPECT_NEAR(CubicBSplineKernel<2>::Gradient(0.5), -0.625, 1e-12); + EXPECT_NEAR(CubicBSplineKernel<2>::Gradient(-0.5), 0.625, 1e-12); +} + +TEST(CubicBSplineKernel, Stencil) +{ + ExpectStencilPartitionAndGradient<2>(); + ExpectStencilPartitionAndGradient<3>(); +} + +TEST(CubicBSplineKernel, RejectsUnrepresentableCoordinates) +{ + ExpectStencilRejectsUnrepresentableCoordinates<2>(); + ExpectStencilRejectsUnrepresentableCoordinates<3>(); +} + +TEST(MPMSystemData, ParticleStateResizes) +{ + ExpectParticleStateResizes<2>(); + ExpectParticleStateResizes<3>(); +} + +TEST(MPMSystemData, BaseSetResizesMPMState) +{ + ExpectBaseSetResizesMPMState<2>(); + ExpectBaseSetResizesMPMState<3>(); +} + +TEST(MPMSystemData, BaseDeserializeResizesMPMState) +{ + ExpectBaseDeserializeResizesMPMState<2>(); + ExpectBaseDeserializeResizesMPMState<3>(); +} + +TEST(MPMSystemData, GridStateResizes) +{ + ExpectGridStateResizes<2>(); + ExpectGridStateResizes<3>(); +} + +TEST(MPMSystemData, ParticleToGridConservesMassAndMomentum) +{ + ExpectParticleToGridConservation<2>(Vector2D::MakeConstant(1.25)); + ExpectParticleToGridConservation<3>(Vector3D::MakeConstant(1.25)); + ExpectParticleToGridConservation<2>(Vector2D{}); + ExpectParticleToGridConservation<3>(Vector3D{}); +} + +TEST(MPMSystemData, GridToParticleBlendsPICAndFLIP) +{ + ExpectGridToParticleBlend<2>(0.0, 3.0, Vector2D::MakeConstant(1.25)); + ExpectGridToParticleBlend<3>(0.0, 3.0, Vector3D::MakeConstant(1.25)); + ExpectGridToParticleBlend<2>(1.0, 12.0, Vector2D{}); + ExpectGridToParticleBlend<3>(1.0, 12.0, Vector3D{}); + ExpectGridToParticleBlend<2>(0.95, 11.55, Vector2D::MakeConstant(1.25), + true); + ExpectGridToParticleBlend<3>(0.95, 11.55, Vector3D::MakeConstant(1.25), + true); +} + +TEST(MPMSystemData, EmptyTransfersAreSafe) +{ + MPMSystemData2 data2; + MPMSystemData3 data3; + + EXPECT_NO_THROW(data2.TransferFromParticlesToGrid()); + EXPECT_NO_THROW(data2.TransferFromGridToParticles()); + EXPECT_NO_THROW(data3.TransferFromParticlesToGrid()); + EXPECT_NO_THROW(data3.TransferFromGridToParticles()); +} + +TEST(MPMSystemData, RejectsDivergentGridState) +{ + ExpectRejectsDivergentGridState<2>(); + ExpectRejectsDivergentGridState<3>(); +} + +TEST(MPMSystemData, InvalidGridLeavesParticleVelocitiesUnchanged) +{ + ExpectInvalidGridLeavesParticleVelocitiesUnchanged<2>(); + ExpectInvalidGridLeavesParticleVelocitiesUnchanged<3>(); +} + +TEST(MPMSystemData, RejectsInvalidInput) +{ + ExpectRejectsInvalidInput<2>(); + ExpectRejectsInvalidInput<3>(); +}