From d0040a8ce12c62f16415164d6e5859cfbe5475a2 Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Mon, 24 Aug 2026 21:05:07 +0900 Subject: [PATCH 1/8] refactor: extract shared MPM transfer state --- .../Core/Particle/MPM/MPMSystemData-Impl.hpp | 241 +++++++++++++----- Includes/Core/Particle/MPM/MPMSystemData.hpp | 84 ++++-- 2 files changed, 247 insertions(+), 78 deletions(-) diff --git a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp index 0577661083..3232811c66 100644 --- a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp +++ b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace CubbyFlow { @@ -158,10 +159,9 @@ CubicBSplineKernel::Stencil CubicBSplineKernel::GetStencil( } template -MPMSystemData::MPMSystemData(const Vector& resolution, - const Vector& gridSpacing, - const Vector& gridOrigin, - size_t numberOfParticles) +MPMTransferSystemData::MPMTransferSystemData( + const Vector& resolution, const Vector& gridSpacing, + const Vector& gridOrigin, size_t numberOfParticles) : Base{} { ResizeGrid(resolution, gridSpacing, gridOrigin); @@ -169,33 +169,32 @@ MPMSystemData::MPMSystemData(const Vector& resolution, } template -void MPMSystemData::Resize(size_t newNumberOfParticles) +void MPMTransferSystemData::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) +void MPMTransferSystemData::Deserialize(const std::vector& buffer) { Base::Deserialize(buffer); - ResetMPMState(); + ResetTransferState(); } template -void MPMSystemData::Set(const ParticleSystemData& other) +void MPMTransferSystemData::Set(const ParticleSystemData& other) { Base::Set(other); - ResetMPMState(); + ResetTransferState(); } template -void MPMSystemData::ResizeGrid(const Vector& resolution, - const Vector& gridSpacing, - const Vector& gridOrigin) +void MPMTransferSystemData::ResizeGrid(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin) { ValidateGridParameters(resolution, gridSpacing, gridOrigin); @@ -205,88 +204,76 @@ void MPMSystemData::ResizeGrid(const Vector& resolution, } template -ConstArrayView1 MPMSystemData::ParticleMasses() const +ConstArrayView1 MPMTransferSystemData::ParticleMasses() const { return m_particleMasses.View(); } template -ArrayView1 MPMSystemData::ParticleMasses() +ArrayView1 MPMTransferSystemData::ParticleMasses() { return m_particleMasses.View(); } template -ConstArrayView1 MPMSystemData::InitialVolumes() const +ConstArrayView1 MPMTransferSystemData::InitialVolumes() const { return m_initialVolumes.View(); } template -ArrayView1 MPMSystemData::InitialVolumes() +ArrayView1 MPMTransferSystemData::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 +const VertexCenteredScalarGrid& MPMTransferSystemData::GridMass() const { return m_gridMass; } template -VertexCenteredScalarGrid& MPMSystemData::GridMass() +VertexCenteredScalarGrid& MPMTransferSystemData::GridMass() { return m_gridMass; } template -const VertexCenteredVectorGrid& MPMSystemData::GridVelocities() const +const VertexCenteredVectorGrid& MPMTransferSystemData::GridVelocities() + const { return m_gridVelocities; } template -VertexCenteredVectorGrid& MPMSystemData::GridVelocities() +VertexCenteredVectorGrid& MPMTransferSystemData::GridVelocities() { return m_gridVelocities; } template const VertexCenteredVectorGrid& -MPMSystemData::GridVelocitiesBeforeUpdate() const +MPMTransferSystemData::GridVelocitiesBeforeUpdate() const { return m_gridVelocitiesBeforeUpdate; } template -VertexCenteredVectorGrid& MPMSystemData::GridVelocitiesBeforeUpdate() +VertexCenteredVectorGrid& +MPMTransferSystemData::GridVelocitiesBeforeUpdate() { return m_gridVelocitiesBeforeUpdate; } template -double MPMSystemData::FLIPBlendingFactor() const +double MPMTransferSystemData::FLIPBlendingFactor() const { return m_flipBlendingFactor; } template -void MPMSystemData::SetFLIPBlendingFactor(double factor) +void MPMTransferSystemData::SetFLIPBlendingFactor(double factor) { if (!std::isfinite(factor) || factor < 0.0 || factor > 1.0) { @@ -297,7 +284,7 @@ void MPMSystemData::SetFLIPBlendingFactor(double factor) } template -void MPMSystemData::TransferFromParticlesToGrid() +void MPMTransferSystemData::TransferFromParticlesToGrid() { ValidateGridState(); @@ -313,12 +300,17 @@ void MPMSystemData::TransferFromParticlesToGrid() } } - m_gridMass.Fill(0.0, ExecutionPolicy::Serial); - m_gridVelocities.Fill(Vector{}, ExecutionPolicy::Serial); + VertexCenteredScalarGrid nextGridMass{ m_gridMass.Resolution(), + m_gridMass.GridSpacing(), + m_gridMass.Origin() }; + VertexCenteredVectorGrid nextGridVelocities{ + m_gridVelocities.Resolution(), m_gridVelocities.GridSpacing(), + m_gridVelocities.Origin() + }; - const auto dataSize = m_gridMass.DataSize(); - const auto gridSpacing = m_gridMass.GridSpacing(); - const auto dataOrigin = m_gridMass.DataOrigin(); + const auto dataSize = nextGridMass.DataSize(); + const auto gridSpacing = nextGridMass.GridSpacing(); + const auto dataOrigin = nextGridMass.DataOrigin(); for (size_t i = 0; i < this->NumberOfParticles(); ++i) { @@ -335,29 +327,55 @@ void MPMSystemData::TransferFromParticlesToGrid() 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]; + nextGridMass(index) += mass; + nextGridVelocities(index) += mass * velocities[i]; } } - m_gridMass.ForEachDataPointIndex([this](const Vector& index) { - const double mass = m_gridMass(index); + nextGridMass.ForEachDataPointIndex([&nextGridMass, &nextGridVelocities]( + const Vector& index) { + const double mass = nextGridMass(index); + + if (!std::isfinite(mass) || + !MPMTransferSystemData::IsFinite(nextGridVelocities(index))) + { + throw std::invalid_argument("Invalid MPM grid accumulation."); + } + if (mass > 0.0) { - m_gridVelocities(index) /= mass; + nextGridVelocities(index) /= mass; + + if (!MPMTransferSystemData::IsFinite(nextGridVelocities(index))) + { + throw std::invalid_argument( + "Invalid MPM grid velocity update."); + } } }); - m_gridVelocitiesBeforeUpdate.Set(m_gridVelocities); + VertexCenteredVectorGrid nextGridVelocitiesBeforeUpdate = + nextGridVelocities; + + m_gridMass = std::move(nextGridMass); + m_gridVelocities = std::move(nextGridVelocities); + m_gridVelocitiesBeforeUpdate = std::move(nextGridVelocitiesBeforeUpdate); } template -void MPMSystemData::TransferFromGridToParticles() +void MPMTransferSystemData::TransferFromGridToParticles() +{ + ValidateGridToParticleState(); + TransferFromGridToParticlesUnchecked(); +} + +template +void MPMTransferSystemData::ValidateGridToParticleState() const { ValidateGridState(); const auto positions = this->Positions(); - auto velocities = this->Velocities(); + const auto velocities = this->Velocities(); for (size_t i = 0; i < this->NumberOfParticles(); ++i) { @@ -375,6 +393,14 @@ void MPMSystemData::TransferFromGridToParticles() throw std::invalid_argument("Invalid MPM grid velocity."); } }); +} + +template +void MPMTransferSystemData::TransferFromGridToParticlesUnchecked() +{ + const auto positions = this->Positions(); + auto velocities = this->Velocities(); + Array1> nextVelocities(this->NumberOfParticles()); const auto dataSize = m_gridVelocities.DataSize(); const auto gridSpacing = m_gridVelocities.GridSpacing(); @@ -405,12 +431,56 @@ void MPMSystemData::TransferFromGridToParticles() (1.0 - m_flipBlendingFactor) * picVelocity + m_flipBlendingFactor * flipVelocity; - velocities[i] = result; + if (!IsFinite(result)) + { + throw std::invalid_argument( + "Invalid MPM particle velocity update."); + } + + nextVelocities[i] = result; + } + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + velocities[i] = nextVelocities[i]; } } template -Vector MPMSystemData::ClampIndex( +Matrix MPMTransferSystemData::ComputeVelocityGradient( + size_t particleIndex) const +{ + const auto positions = this->Positions(); + const auto dataSize = m_gridVelocities.DataSize(); + const auto stencil = CubicBSplineKernel::GetStencil( + positions[particleIndex], m_gridVelocities.GridSpacing(), + m_gridVelocities.DataOrigin()); + Matrix result; + + for (const auto& entry : stencil) + { + if (entry.weight == 0.0) + { + continue; + } + + const Vector velocity = + m_gridVelocities(ClampIndex(entry.index, dataSize)); + + for (size_t row = 0; row < N; ++row) + { + for (size_t column = 0; column < N; ++column) + { + result(row, column) += velocity[row] * entry.gradient[column]; + } + } + } + + return result; +} + +template +Vector MPMTransferSystemData::ClampIndex( const Vector& index, const Vector& dataSize) { Vector result; @@ -427,7 +497,7 @@ Vector MPMSystemData::ClampIndex( } template -bool MPMSystemData::IsFinite(const Vector& value) +bool MPMTransferSystemData::IsFinite(const Vector& value) { for (size_t axis = 0; axis < N; ++axis) { @@ -441,7 +511,7 @@ bool MPMSystemData::IsFinite(const Vector& value) } template -void MPMSystemData::ValidateGridParameters( +void MPMTransferSystemData::ValidateGridParameters( const Vector& resolution, const Vector& gridSpacing, const Vector& gridOrigin) { @@ -458,7 +528,7 @@ void MPMSystemData::ValidateGridParameters( } template -void MPMSystemData::ValidateGridState() const +void MPMTransferSystemData::ValidateGridState() const { ValidateGridParameters(m_gridMass.Resolution(), m_gridMass.GridSpacing(), m_gridMass.Origin()); @@ -482,16 +552,67 @@ void MPMSystemData::ValidateGridState() const } template -void MPMSystemData::ResetMPMState() +void MPMTransferSystemData::ResetTransferState() { 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); } + +template +MPMSystemData::MPMSystemData(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin, + size_t numberOfParticles) + : TransferBase{ resolution, gridSpacing, gridOrigin, numberOfParticles }, + m_deformationStates{ numberOfParticles, DeformationState{} } +{ + // Do nothing +} + +template +void MPMSystemData::Resize(size_t newNumberOfParticles) +{ + TransferBase::Resize(newNumberOfParticles); + m_deformationStates.Resize(newNumberOfParticles, DeformationState{}); +} + +template +void MPMSystemData::Deserialize(const std::vector& buffer) +{ + TransferBase::Deserialize(buffer); + m_deformationStates.Fill(DeformationState{}); +} + +template +void MPMSystemData::Set(const ParticleSystemData& other) +{ + TransferBase::Set(other); + m_deformationStates.Fill(DeformationState{}); +} + +template +void MPMSystemData::TransferFromGridToParticles() +{ + TransferBase::TransferFromGridToParticles(); +} + +template +ConstArrayView1::DeformationState> +MPMSystemData::DeformationStates() const +{ + return m_deformationStates.View(); +} + +template +ArrayView1::DeformationState> +MPMSystemData::DeformationStates() +{ + return m_deformationStates.View(); +} } // namespace CubbyFlow #endif diff --git a/Includes/Core/Particle/MPM/MPMSystemData.hpp b/Includes/Core/Particle/MPM/MPMSystemData.hpp index 008c6ca6bf..25b5c4407c 100644 --- a/Includes/Core/Particle/MPM/MPMSystemData.hpp +++ b/Includes/Core/Particle/MPM/MPMSystemData.hpp @@ -62,22 +62,21 @@ class CubicBSplineKernel final }; //! -//! \brief N-D material point method particle and grid state. +//! \brief Shared N-D material point method particle-grid transfer state. //! //! \note Serialization preserves only the inherited particle-system state; -//! MPM-specific particle and grid state is reset after deserialization. +//! transfer-specific particle and grid state is reset after deserialization. //! template -class MPMSystemData final : public ParticleSystemData +class MPMTransferSystemData : 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( + //! Constructs transfer state with a vertex-centered background grid. + explicit MPMTransferSystemData( const Vector& resolution = Vector::MakeConstant(1), const Vector& gridSpacing = @@ -85,13 +84,13 @@ class MPMSystemData final : public ParticleSystemData const Vector& gridOrigin = Vector{}, size_t numberOfParticles = 0); - //! Resizes particle state, initializing new MPM attributes. + //! Resizes particle state, initializing new transfer attributes. void Resize(size_t newNumberOfParticles) override; - //! Deserializes inherited particle state and resets MPM-specific state. + //! Deserializes inherited particle state and resets transfer state. void Deserialize(const std::vector& buffer) override; - //! Copies inherited particle state and resets MPM-specific state. + //! Copies inherited particle state and resets transfer state. void Set(const ParticleSystemData& other) override; //! Resizes the background grid without changing particle state. @@ -111,12 +110,6 @@ class MPMSystemData final : public ParticleSystemData //! 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; @@ -146,11 +139,11 @@ class MPMSystemData final : public ParticleSystemData //! Stencil nodes outside the finite grid are clamped to its boundary. void TransferFromParticlesToGrid(); + protected: //! 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); @@ -162,17 +155,72 @@ class MPMSystemData final : public ParticleSystemData void ValidateGridState() const; - void ResetMPMState(); + void ValidateGridToParticleState() const; + + void TransferFromGridToParticlesUnchecked(); + + [[nodiscard]] Matrix ComputeVelocityGradient( + size_t particleIndex) const; + + private: + void ResetTransferState(); Array1 m_particleMasses; Array1 m_initialVolumes; - Array1 m_deformationStates; VertexCenteredScalarGrid m_gridMass; VertexCenteredVectorGrid m_gridVelocities; VertexCenteredVectorGrid m_gridVelocitiesBeforeUpdate; double m_flipBlendingFactor = 0.95; }; +//! +//! \brief N-D snow 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 MPMTransferSystemData +{ + public: + static_assert(N == 2 || N == 3, "MPM supports only 2-D and 3-D."); + + using Base = ParticleSystemData; + using TransferBase = MPMTransferSystemData; + using DeformationState = SnowDeformationState; + + //! Constructs snow 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 deformation states. + void Resize(size_t newNumberOfParticles) override; + + //! Deserializes inherited particle state and resets deformation state. + void Deserialize(const std::vector& buffer) override; + + //! Copies inherited particle state and resets deformation state. + void Set(const ParticleSystemData& other) override; + + //! Transfers updated background-grid velocities to particles. + //! Stencil nodes outside the finite grid are clamped to its boundary. + void TransferFromGridToParticles(); + + //! Returns per-particle deformation states. + [[nodiscard]] ConstArrayView1 DeformationStates() const; + + //! Returns per-particle deformation states. + [[nodiscard]] ArrayView1 DeformationStates(); + + private: + Array1 m_deformationStates; +}; + //! 2-D material point method system data. using MPMSystemData2 = MPMSystemData<2>; From 5693a11eb13def4738f6532d7ae308c54737a5ea Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Mon, 24 Aug 2026 21:05:48 +0900 Subject: [PATCH 2/8] feat: add fluid MPM transfer state --- .../Particle/MPM/MPMFluidSystemData-Impl.hpp | 133 +++++ .../Core/Particle/MPM/MPMFluidSystemData.hpp | 93 ++++ Tests/UnitTests/MPMFluidSystemDataTests.cpp | 474 ++++++++++++++++++ 3 files changed, 700 insertions(+) create mode 100644 Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp create mode 100644 Includes/Core/Particle/MPM/MPMFluidSystemData.hpp create mode 100644 Tests/UnitTests/MPMFluidSystemDataTests.cpp diff --git a/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp new file mode 100644 index 0000000000..f8787b746e --- /dev/null +++ b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp @@ -0,0 +1,133 @@ +// 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_FLUID_SYSTEM_DATA_IMPL_HPP +#define CUBBYFLOW_MPM_FLUID_SYSTEM_DATA_IMPL_HPP + +#include +#include + +namespace CubbyFlow +{ +template +MPMFluidSystemData::MPMFluidSystemData(const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin, + size_t numberOfParticles) + : Base{ resolution, gridSpacing, gridOrigin, numberOfParticles }, + m_volumeRatios(numberOfParticles, 1.0), + m_velocityGradients(numberOfParticles, MatrixType{}) +{ + // Do nothing +} + +template +void MPMFluidSystemData::Resize(size_t newNumberOfParticles) +{ + Base::Resize(newNumberOfParticles); + + m_volumeRatios.Resize(newNumberOfParticles, 1.0); + m_velocityGradients.Resize(newNumberOfParticles, MatrixType{}); +} + +template +void MPMFluidSystemData::Deserialize(const std::vector& buffer) +{ + Base::Deserialize(buffer); + ResetFluidState(); +} + +template +void MPMFluidSystemData::Set(const ParticleSystemData& other) +{ + Base::Set(other); + ResetFluidState(); +} + +template +ConstArrayView1 MPMFluidSystemData::VolumeRatios() const +{ + return m_volumeRatios.View(); +} + +template +ArrayView1 MPMFluidSystemData::VolumeRatios() +{ + return m_volumeRatios.View(); +} + +template +ConstArrayView1::MatrixType> +MPMFluidSystemData::VelocityGradients() const +{ + return m_velocityGradients.View(); +} + +template +ArrayView1::MatrixType> +MPMFluidSystemData::VelocityGradients() +{ + return m_velocityGradients.View(); +} + +template +void MPMFluidSystemData::TransferFromGridToParticles( + double timeStepInSeconds) +{ + if (!std::isfinite(timeStepInSeconds) || timeStepInSeconds < 0.0) + { + throw std::invalid_argument("Invalid MPM fluid time step."); + } + + this->ValidateGridToParticleState(); + + Array1 nextVolumeRatios(this->NumberOfParticles(), 1.0); + Array1 nextVelocityGradients(this->NumberOfParticles(), + MatrixType{}); + + for (size_t i = 0; i < this->NumberOfParticles(); ++i) + { + if (!std::isfinite(m_volumeRatios[i]) || m_volumeRatios[i] <= 0.0) + { + throw std::invalid_argument("Invalid MPM fluid volume ratio."); + } + + const MatrixType gradient = this->ComputeVelocityGradient(i); + const double volumeIncrement = + (MatrixType::MakeIdentity() + timeStepInSeconds * gradient) + .Determinant(); + const double volumeRatio = m_volumeRatios[i] * volumeIncrement; + + if (!std::isfinite(volumeIncrement) || volumeIncrement <= 0.0 || + !std::isfinite(volumeRatio) || volumeRatio <= 0.0) + { + throw std::invalid_argument("Invalid MPM fluid volume update."); + } + + nextVelocityGradients[i] = gradient; + nextVolumeRatios[i] = volumeRatio; + } + + this->TransferFromGridToParticlesUnchecked(); + + m_volumeRatios.Swap(nextVolumeRatios); + m_velocityGradients.Swap(nextVelocityGradients); +} + +template +void MPMFluidSystemData::ResetFluidState() +{ + m_volumeRatios.Fill(1.0); + m_velocityGradients.Fill(MatrixType{}); +} + +} // namespace CubbyFlow + +#endif diff --git a/Includes/Core/Particle/MPM/MPMFluidSystemData.hpp b/Includes/Core/Particle/MPM/MPMFluidSystemData.hpp new file mode 100644 index 0000000000..fac13d3d44 --- /dev/null +++ b/Includes/Core/Particle/MPM/MPMFluidSystemData.hpp @@ -0,0 +1,93 @@ +// 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_FLUID_SYSTEM_DATA_HPP +#define CUBBYFLOW_MPM_FLUID_SYSTEM_DATA_HPP + +#include + +namespace CubbyFlow +{ +//! +//! \brief N-D weakly compressible MPM fluid transfer state. +//! +//! Stores the particle volume ratio and velocity gradient required by a fluid +//! constitutive model without carrying snow deformation or plasticity state. +//! +template +class MPMFluidSystemData final : public MPMTransferSystemData +{ + public: + static_assert(N == 2 || N == 3, "MPM supports only 2-D and 3-D."); + + using Base = MPMTransferSystemData; + using MatrixType = Matrix; + + //! Constructs fluid MPM state with a vertex-centered background grid. + explicit MPMFluidSystemData( + 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 fluid attributes. + void Resize(size_t newNumberOfParticles) override; + + //! Deserializes inherited particle state and resets fluid state. + void Deserialize(const std::vector& buffer) override; + + //! Copies inherited particle state and resets fluid state. + void Set(const ParticleSystemData& other) override; + + //! Returns current-to-reference particle volume ratios. + [[nodiscard]] ConstArrayView1 VolumeRatios() const; + + //! Returns current-to-reference particle volume ratios. + [[nodiscard]] ArrayView1 VolumeRatios(); + + //! Returns particle velocity gradients interpolated from the grid. + [[nodiscard]] ConstArrayView1 VelocityGradients() const; + + //! Returns particle velocity gradients interpolated from the grid. + [[nodiscard]] ArrayView1 VelocityGradients(); + + //! + //! Transfers updated grid velocities and advances fluid kinematic state. + //! + //! The volume ratio is updated by the determinant of the incremental + //! deformation gradient, I + dt * velocityGradient. + //! + void TransferFromGridToParticles(double timeStepInSeconds); + + private: + void ResetFluidState(); + + Array1 m_volumeRatios; + Array1 m_velocityGradients; +}; + +//! 2-D weakly compressible MPM fluid system data. +using MPMFluidSystemData2 = MPMFluidSystemData<2>; + +//! 3-D weakly compressible MPM fluid system data. +using MPMFluidSystemData3 = MPMFluidSystemData<3>; + +//! Shared pointer type of MPMFluidSystemData2. +using MPMFluidSystemData2Ptr = std::shared_ptr; + +//! Shared pointer type of MPMFluidSystemData3. +using MPMFluidSystemData3Ptr = std::shared_ptr; +} // namespace CubbyFlow + +#include + +#endif diff --git a/Tests/UnitTests/MPMFluidSystemDataTests.cpp b/Tests/UnitTests/MPMFluidSystemDataTests.cpp new file mode 100644 index 0000000000..3adb137a59 --- /dev/null +++ b/Tests/UnitTests/MPMFluidSystemDataTests.cpp @@ -0,0 +1,474 @@ +// 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 +#include + +using namespace CubbyFlow; + +namespace +{ +template +concept HasDeformationStates = requires(T& data) { data.DeformationStates(); }; + +template +concept HasNoArgumentGridToParticleTransfer = + requires(T& data) { data.TransferFromGridToParticles(); }; + +static_assert(!HasDeformationStates); +static_assert(!HasDeformationStates); +static_assert(!HasNoArgumentGridToParticleTransfer>); +static_assert(!HasNoArgumentGridToParticleTransfer>); +static_assert(HasNoArgumentGridToParticleTransfer); +static_assert(HasNoArgumentGridToParticleTransfer); +static_assert(std::is_same_v); +static_assert(std::is_same_v); + +template +void ExpectParticleStateResizes() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + + EXPECT_DOUBLE_EQ(data.VolumeRatios()[0], 1.0); + EXPECT_TRUE(data.VelocityGradients()[0].IsSimilar(Matrix{})); + + data.VolumeRatios()[0] = 2.0; + data.VelocityGradients()[0](0, 0) = 3.0; + data.AddParticle(Vector::MakeConstant(0.25)); + + ASSERT_EQ(data.VolumeRatios().Length(), 2u); + ASSERT_EQ(data.VelocityGradients().Length(), 2u); + EXPECT_DOUBLE_EQ(data.VolumeRatios()[0], 2.0); + EXPECT_DOUBLE_EQ(data.VolumeRatios()[1], 1.0); + EXPECT_DOUBLE_EQ(data.VelocityGradients()[0](0, 0), 3.0); + EXPECT_TRUE(data.VelocityGradients()[1].IsSimilar(Matrix{})); +} + +template +void DirtyFluidState(MPMFluidSystemData* data) +{ + data->VolumeRatios()[0] = 2.0; + data->VelocityGradients()[0](0, 0) = 3.0; +} + +template +void ExpectFluidStateReset(const MPMFluidSystemData& data) +{ + ASSERT_EQ(data.VolumeRatios().Length(), 3u); + ASSERT_EQ(data.VelocityGradients().Length(), 3u); + + for (size_t i = 0; i < 3; ++i) + { + EXPECT_DOUBLE_EQ(data.VolumeRatios()[i], 1.0); + EXPECT_TRUE( + data.VelocityGradients()[i].IsSimilar(Matrix{})); + } +} + +template +void ExpectBaseSetResetsFluidState() +{ + MPMFluidSystemData data{ Vector::MakeConstant(2), + Vector::MakeConstant(1.0), + {}, + 1 }; + ParticleSystemData source{ 3 }; + + DirtyFluidState(&data); + data.Set(source); + ExpectFluidStateReset(data); +} + +template +void ExpectBaseDeserializeResetsFluidState() +{ + ParticleSystemData source{ 3 }; + + std::vector buffer; + source.Serialize(&buffer); + + MPMFluidSystemData data{ Vector::MakeConstant(2), + Vector::MakeConstant(1.0), + {}, + 1 }; + + DirtyFluidState(&data); + data.Deserialize(buffer); + ExpectFluidStateReset(data); +} + +template +void ExpectParticleToGridConservation() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 2 }; + data.Positions()[0] = Vector::MakeConstant(1.25); + 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 totalMass = 0.0; + Vector totalMomentum; + data.GridMass().ForEachDataPointIndex( + [&data, &totalMass, &totalMomentum](const Vector& index) { + const double mass = data.GridMass()(index); + totalMass += mass; + totalMomentum += mass * data.GridVelocities()(index); + }); + + EXPECT_NEAR(totalMass, 5.0, 1e-11); + EXPECT_TRUE( + totalMomentum.IsSimilar(Vector::MakeConstant(1.0), 1e-11)); +} + +template +void ExpectParticleToGridFailureDoesNotCommit() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 2 }; + const Vector gridVelocity = Vector::MakeConstant(3.0); + const Vector oldGridVelocity = + Vector::MakeConstant(4.0); + data.Positions()[0] = Vector::MakeConstant(1.25); + data.Positions()[1] = + Vector::MakeConstant(std::numeric_limits::max()); + data.GridMass().Fill(2.0, ExecutionPolicy::Serial); + data.GridVelocities().Fill(gridVelocity, ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Fill(oldGridVelocity, + ExecutionPolicy::Serial); + + EXPECT_THROW(data.TransferFromParticlesToGrid(), std::invalid_argument); + + data.GridMass().ForEachDataPointIndex( + [&data, &gridVelocity, + &oldGridVelocity](const Vector& index) { + EXPECT_DOUBLE_EQ(data.GridMass()(index), 2.0); + EXPECT_TRUE(data.GridVelocities()(index).IsSimilar(gridVelocity)); + EXPECT_TRUE(data.GridVelocitiesBeforeUpdate()(index).IsSimilar( + oldGridVelocity)); + }); +} + +template +void ExpectOverflowingParticleToGridTransferDoesNotCommit() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + const Vector gridVelocity = Vector::MakeConstant(3.0); + const Vector oldGridVelocity = + Vector::MakeConstant(4.0); + + data.Positions()[0] = Vector::MakeConstant(1.25); + data.Velocities()[0] = + Vector::MakeConstant(std::numeric_limits::max()); + data.ParticleMasses()[0] = std::numeric_limits::max(); + data.GridMass().Fill(2.0, ExecutionPolicy::Serial); + data.GridVelocities().Fill(gridVelocity, ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Fill(oldGridVelocity, + ExecutionPolicy::Serial); + + EXPECT_THROW(data.TransferFromParticlesToGrid(), std::invalid_argument); + + data.GridMass().ForEachDataPointIndex( + [&data, &gridVelocity, + &oldGridVelocity](const Vector& index) { + EXPECT_DOUBLE_EQ(data.GridMass()(index), 2.0); + EXPECT_TRUE(data.GridVelocities()(index).IsSimilar(gridVelocity)); + EXPECT_TRUE(data.GridVelocitiesBeforeUpdate()(index).IsSimilar( + oldGridVelocity)); + }); +} + +template +void ExpectUniformVelocityRoundTrip() +{ + const Vector velocity = Vector::MakeConstant(2.5); + MPMFluidSystemData data{ Vector::MakeConstant(6), + Vector::MakeConstant(1.0), + {}, + 2 }; + data.Positions()[0] = Vector::MakeConstant(2.25); + data.Positions()[1] = Vector::MakeConstant(3.25); + data.Velocities()[0] = velocity; + data.Velocities()[1] = velocity; + + data.TransferFromParticlesToGrid(); + data.TransferFromGridToParticles(0.1); + + EXPECT_TRUE(data.Velocities()[0].IsSimilar(velocity, 1e-12)); + EXPECT_TRUE(data.Velocities()[1].IsSimilar(velocity, 1e-12)); + EXPECT_NEAR(data.VelocityGradients()[0].AbsMax(), 0.0, 1e-12); + EXPECT_NEAR(data.VelocityGradients()[1].AbsMax(), 0.0, 1e-12); + EXPECT_NEAR(data.VolumeRatios()[0], 1.0, 1e-12); + EXPECT_NEAR(data.VolumeRatios()[1], 1.0, 1e-12); +} + +template +void ExpectUniformDilation(double rate, double expectedVolumeRatio) +{ + MPMFluidSystemData data{ Vector::MakeConstant(6), + Vector::MakeConstant(1.0), + {}, + 1 }; + data.Positions()[0] = Vector::MakeConstant(2.25); + data.GridVelocities().Fill( + [rate](const Vector& position) { return rate * position; }, + ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Set(data.GridVelocities()); + + data.TransferFromGridToParticles(1.0); + + const auto& gradient = data.VelocityGradients()[0]; + + for (size_t row = 0; row < N; ++row) + { + for (size_t column = 0; column < N; ++column) + { + EXPECT_NEAR(gradient(row, column), row == column ? rate : 0.0, + 1e-12); + } + } + + EXPECT_NEAR(data.VolumeRatios()[0], expectedVolumeRatio, 1e-12); +} + +template +void FillCrossVelocityField(MPMFluidSystemData* data) +{ + data->GridVelocities().Fill( + [](const Vector& position) { + Vector velocity; + velocity[0] = 2.0 * position[1]; + velocity[1] = 3.0 * position[0]; + return velocity; + }, + ExecutionPolicy::Serial); + data->GridVelocitiesBeforeUpdate().Set(data->GridVelocities()); +} + +template +void ExpectVelocityGradientPreservesComponentOrientation() +{ + MPMFluidSystemData data{ Vector::MakeConstant(6), + Vector::MakeConstant(1.0), + {}, + 1 }; + data.Positions()[0] = Vector::MakeConstant(2.25); + + FillCrossVelocityField(&data); + data.TransferFromGridToParticles(0.0); + + const auto& gradient = data.VelocityGradients()[0]; + + for (size_t row = 0; row < N; ++row) + { + for (size_t column = 0; column < N; ++column) + { + double expected = 0.0; + + if (row == 0 && column == 1) + { + expected = 2.0; + } + else if (row == 1 && column == 0) + { + expected = 3.0; + } + + EXPECT_NEAR(gradient(row, column), expected, 1e-12); + } + } +} + +template +void ExpectVolumeRatioMultipliesNonDiagonalIncrement() +{ + MPMFluidSystemData data{ Vector::MakeConstant(6), + Vector::MakeConstant(1.0), + {}, + 1 }; + data.Positions()[0] = Vector::MakeConstant(2.25); + data.VolumeRatios()[0] = 2.0; + + FillCrossVelocityField(&data); + + data.TransferFromGridToParticles(0.1); + EXPECT_NEAR(data.VolumeRatios()[0], 1.88, 1e-12); + + data.TransferFromGridToParticles(0.1); + EXPECT_NEAR(data.VolumeRatios()[0], 1.7672, 1e-12); +} + +template +void ExpectEmptyAndBoundaryTransfersAreSafe() +{ + MPMFluidSystemData empty; + EXPECT_NO_THROW(empty.TransferFromParticlesToGrid()); + EXPECT_NO_THROW(empty.TransferFromGridToParticles(0.1)); + + MPMFluidSystemData boundary{ Vector::MakeConstant(2), + Vector::MakeConstant(1.0), + {}, + 1 }; + boundary.Velocities()[0] = Vector::MakeConstant(1.0); + + EXPECT_NO_THROW(boundary.TransferFromParticlesToGrid()); + EXPECT_NO_THROW(boundary.TransferFromGridToParticles(0.1)); + EXPECT_TRUE(boundary.Velocities()[0].IsSimilar( + Vector::MakeConstant(1.0), 1e-12)); + EXPECT_NEAR(boundary.VolumeRatios()[0], 1.0, 1e-12); +} + +template +void ExpectInvalidFluidStateIsNotCommitted() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + const Vector velocity = Vector::MakeConstant(4.0); + + data.Positions()[0] = Vector::MakeConstant(1.25); + data.Velocities()[0] = velocity; + data.VolumeRatios()[0] = 0.0; + data.GridVelocities().Fill(Vector::MakeConstant(2.0), + ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Fill(Vector::MakeConstant(1.0), + ExecutionPolicy::Serial); + + EXPECT_THROW(data.TransferFromGridToParticles(0.1), std::invalid_argument); + EXPECT_TRUE(data.Velocities()[0].IsSimilar(velocity)); + EXPECT_DOUBLE_EQ(data.VolumeRatios()[0], 0.0); + EXPECT_TRUE(data.VelocityGradients()[0].IsSimilar(Matrix{})); + EXPECT_THROW(data.TransferFromGridToParticles(-0.1), std::invalid_argument); +} + +template +void ExpectOverflowingGridTransferIsNotCommitted() +{ + MPMFluidSystemData data{ Vector::MakeConstant(4), + Vector::MakeConstant(1.0), + {}, + 1 }; + const Vector velocity = Vector::MakeConstant(4.0); + const double large = 0.6 * std::numeric_limits::max(); + data.Positions()[0] = Vector::MakeConstant(1.25); + data.Velocities()[0] = velocity; + data.VolumeRatios()[0] = 2.0; + data.VelocityGradients()[0](0, 0) = 3.0; + data.GridVelocities().Fill(Vector::MakeConstant(large), + ExecutionPolicy::Serial); + data.GridVelocitiesBeforeUpdate().Fill( + Vector::MakeConstant(-large), ExecutionPolicy::Serial); + data.SetFLIPBlendingFactor(1.0); + + EXPECT_THROW(data.TransferFromGridToParticles(0.0), std::invalid_argument); + EXPECT_TRUE(data.Velocities()[0].IsSimilar(velocity)); + EXPECT_DOUBLE_EQ(data.VolumeRatios()[0], 2.0); + EXPECT_DOUBLE_EQ(data.VelocityGradients()[0](0, 0), 3.0); +} +} // namespace + +TEST(MPMFluidSystemData, ParticleStateResizes) +{ + ExpectParticleStateResizes<2>(); + ExpectParticleStateResizes<3>(); +} + +TEST(MPMFluidSystemData, ParticleToGridConservesMassAndMomentum) +{ + ExpectParticleToGridConservation<2>(); + ExpectParticleToGridConservation<3>(); +} + +TEST(MPMFluidSystemData, ParticleToGridFailureDoesNotCommit) +{ + ExpectParticleToGridFailureDoesNotCommit<2>(); + ExpectParticleToGridFailureDoesNotCommit<3>(); +} + +TEST(MPMFluidSystemData, OverflowingParticleToGridTransferDoesNotCommit) +{ + ExpectOverflowingParticleToGridTransferDoesNotCommit<2>(); + ExpectOverflowingParticleToGridTransferDoesNotCommit<3>(); +} + +TEST(MPMFluidSystemData, BaseSetResetsFluidState) +{ + ExpectBaseSetResetsFluidState<2>(); + ExpectBaseSetResetsFluidState<3>(); +} + +TEST(MPMFluidSystemData, BaseDeserializeResetsFluidState) +{ + ExpectBaseDeserializeResetsFluidState<2>(); + ExpectBaseDeserializeResetsFluidState<3>(); +} + +TEST(MPMFluidSystemData, UniformVelocityRoundTrip) +{ + ExpectUniformVelocityRoundTrip<2>(); + ExpectUniformVelocityRoundTrip<3>(); +} + +TEST(MPMFluidSystemData, UniformExpansionAndCompression) +{ + ExpectUniformDilation<2>(0.1, 1.21); + ExpectUniformDilation<3>(0.1, 1.331); + ExpectUniformDilation<2>(-0.1, 0.81); + ExpectUniformDilation<3>(-0.1, 0.729); +} + +TEST(MPMFluidSystemData, VelocityGradientPreservesComponentOrientation) +{ + ExpectVelocityGradientPreservesComponentOrientation<2>(); + ExpectVelocityGradientPreservesComponentOrientation<3>(); +} + +TEST(MPMFluidSystemData, VolumeRatioMultipliesNonDiagonalIncrement) +{ + ExpectVolumeRatioMultipliesNonDiagonalIncrement<2>(); + ExpectVolumeRatioMultipliesNonDiagonalIncrement<3>(); +} + +TEST(MPMFluidSystemData, EmptyAndBoundaryTransfersAreSafe) +{ + ExpectEmptyAndBoundaryTransfersAreSafe<2>(); + ExpectEmptyAndBoundaryTransfersAreSafe<3>(); +} + +TEST(MPMFluidSystemData, InvalidFluidStateIsNotCommitted) +{ + ExpectInvalidFluidStateIsNotCommitted<2>(); + ExpectInvalidFluidStateIsNotCommitted<3>(); +} + +TEST(MPMFluidSystemData, OverflowingGridTransferIsNotCommitted) +{ + ExpectOverflowingGridTransferIsNotCommitted<2>(); + ExpectOverflowingGridTransferIsNotCommitted<3>(); +} From 258eb1c4b69a92c2ea38c4b76a8224df8bb72d7b Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Mon, 24 Aug 2026 21:06:29 +0900 Subject: [PATCH 3/8] feat: expose fluid MPM state to Python --- .../API/Python/Particle/MPM/MPMSystemData.hpp | 2 + .../API/Python/Particle/MPM/MPMSystemData.cpp | 125 +++++++++++++++--- Sources/API/Python/main.cpp | 2 + Tests/PythonTests/test_mpm_system_data.py | 55 ++++++++ 4 files changed, 165 insertions(+), 19 deletions(-) diff --git a/Includes/API/Python/Particle/MPM/MPMSystemData.hpp b/Includes/API/Python/Particle/MPM/MPMSystemData.hpp index 5fc8a52d17..214a95187e 100644 --- a/Includes/API/Python/Particle/MPM/MPMSystemData.hpp +++ b/Includes/API/Python/Particle/MPM/MPMSystemData.hpp @@ -15,5 +15,7 @@ void AddMPMSystemData2(pybind11::module& m); void AddMPMSystemData3(pybind11::module& m); +void AddMPMFluidSystemData2(pybind11::module& m); +void AddMPMFluidSystemData3(pybind11::module& m); #endif diff --git a/Sources/API/Python/Particle/MPM/MPMSystemData.cpp b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp index f50e63444d..dc34f3de63 100644 --- a/Sources/API/Python/Particle/MPM/MPMSystemData.cpp +++ b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp @@ -9,6 +9,7 @@ // property of any third parties. #include +#include #include #include @@ -23,6 +24,11 @@ template using MPMClass = py::class_, std::shared_ptr>, ParticleSystemData>; +template +using MPMFluidClass = + py::class_, std::shared_ptr>, + ParticleSystemData>; + template py::array_t MatrixView(SnowDeformationState& state, Matrix& matrix) @@ -48,15 +54,38 @@ void AddSnowDeformationState(py::module& m, const char* name) }); } -template -py::array_t ScalarView(MPMSystemData& instance, - ArrayView1 view) +template +py::array_t ScalarView(Data& instance, ArrayView1 view) { return py::array_t({ static_cast(view.Length()) }, { static_cast(sizeof(double)) }, view.data(), py::cast(&instance)); } +template +py::array_t FluidMatrixView(MPMFluidSystemData& instance, + 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(&instance)); +} + +template +py::list VelocityGradients(MPMFluidSystemData& instance) +{ + py::list result; + + for (auto& gradient : instance.VelocityGradients()) + { + result.append(FluidMatrixView(instance, gradient)); + } + + return result; +} + template py::list DeformationStates(MPMSystemData& instance) { @@ -87,34 +116,57 @@ void BindParticleState(MPMClass& cls) } template -void BindGridState(MPMClass& cls) +void BindParticleState(MPMFluidClass& cls) +{ + cls.def_property_readonly("particleMasses", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, + instance.ParticleMasses()); + }) + .def_property_readonly("initialVolumes", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, + instance.InitialVolumes()); + }) + .def_property_readonly("volumeRatios", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, + instance.VolumeRatios()); + }) + .def_property_readonly("velocityGradients", &VelocityGradients); +} + +template +void BindGridState(DataClass& cls) { - cls.def("ResizeGrid", &MPMSystemData::ResizeGrid) + cls.def("ResizeGrid", + [](Data& instance, const Vector& resolution, + const Vector& gridSpacing, + const Vector& gridOrigin) { + instance.ResizeGrid(resolution, gridSpacing, gridOrigin); + }) .def_property_readonly( "gridMass", - [](MPMSystemData& instance) -> auto& { - return instance.GridMass(); - }, + [](Data& instance) -> auto& { return instance.GridMass(); }, py::return_value_policy::reference_internal) .def_property_readonly( "gridVelocities", - [](MPMSystemData& instance) -> auto& { - return instance.GridVelocities(); - }, + [](Data& instance) -> auto& { return instance.GridVelocities(); }, py::return_value_policy::reference_internal) .def_property_readonly( "gridVelocitiesBeforeUpdate", - [](MPMSystemData& instance) -> auto& { + [](Data& instance) -> auto& { return instance.GridVelocitiesBeforeUpdate(); }, py::return_value_policy::reference_internal) - .def_property("flipBlendingFactor", - &MPMSystemData::FLIPBlendingFactor, - &MPMSystemData::SetFLIPBlendingFactor) + .def_property( + "flipBlendingFactor", + [](const Data& instance) { return instance.FLIPBlendingFactor(); }, + [](Data& instance, double factor) { + instance.SetFLIPBlendingFactor(factor); + }) .def("TransferFromParticlesToGrid", - &MPMSystemData::TransferFromParticlesToGrid) - .def("TransferFromGridToParticles", - &MPMSystemData::TransferFromGridToParticles); + [](Data& instance) { instance.TransferFromParticlesToGrid(); }); } template @@ -122,6 +174,7 @@ 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>(), @@ -129,8 +182,32 @@ void AddMPMSystemData(py::module& m, const char* className, py::arg("gridSpacing") = Vector::MakeConstant(1.0), py::arg("gridOrigin") = Vector{}, py::arg("numberOfParticles") = 0); + BindParticleState(cls); - BindGridState(cls); + BindGridState, MPMSystemData>(cls); + + cls.def("TransferFromGridToParticles", [](MPMSystemData& instance) { + instance.TransferFromGridToParticles(); + }); +} + +template +void AddMPMFluidSystemData(py::module& m, const char* className) +{ + MPMFluidClass 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, MPMFluidSystemData>(cls); + + cls.def("TransferFromGridToParticles", + &MPMFluidSystemData::TransferFromGridToParticles, + py::arg("timeStepInSeconds")); } } // namespace @@ -143,3 +220,13 @@ void AddMPMSystemData3(pybind11::module& m) { AddMPMSystemData<3>(m, "MPMSystemData3", "SnowDeformationState3"); } + +void AddMPMFluidSystemData2(pybind11::module& m) +{ + AddMPMFluidSystemData<2>(m, "MPMFluidSystemData2"); +} + +void AddMPMFluidSystemData3(pybind11::module& m) +{ + AddMPMFluidSystemData<3>(m, "MPMFluidSystemData3"); +} diff --git a/Sources/API/Python/main.cpp b/Sources/API/Python/main.cpp index 27f6303537..100d9c12b4 100644 --- a/Sources/API/Python/main.cpp +++ b/Sources/API/Python/main.cpp @@ -222,6 +222,8 @@ PYBIND11_MODULE(pyCubbyFlow, m) AddParticleSystemData3(m); AddMPMSystemData2(m); AddMPMSystemData3(m); + AddMPMFluidSystemData2(m); + AddMPMFluidSystemData3(m); AddSPHSystemData2(m); AddSPHSystemData3(m); diff --git a/Tests/PythonTests/test_mpm_system_data.py b/Tests/PythonTests/test_mpm_system_data.py index f6c391fff7..50e58c690a 100644 --- a/Tests/PythonTests/test_mpm_system_data.py +++ b/Tests/PythonTests/test_mpm_system_data.py @@ -63,3 +63,58 @@ def test_mpm_system_data_api( assert grid.resolution == resized_grid[0] assert grid.gridSpacing == resized_grid[1] assert grid.gridOrigin == resized_grid[2] + + +@pytest.mark.parametrize( + "data_type,resolution,spacing,origin,grid_velocity,dimension", + [ + ( + pyCubbyFlow.MPMFluidSystemData2, + pyCubbyFlow.Vector2UZ(3, 3), + pyCubbyFlow.Vector2D(1.0, 1.0), + pyCubbyFlow.Vector2D(), + (1.0, 1.0), + 2, + ), + ( + pyCubbyFlow.MPMFluidSystemData3, + pyCubbyFlow.Vector3UZ(3, 3, 3), + pyCubbyFlow.Vector3D(1.0, 1.0, 1.0), + pyCubbyFlow.Vector3D(), + (1.0, 1.0, 1.0), + 3, + ), + ], +) +def test_mpm_fluid_system_data_api( + data_type, + resolution, + spacing, + origin, + grid_velocity, + dimension, +): + data = data_type(resolution, spacing, origin, 1) + + ratios = np.asarray(data.volumeRatios) + gradients = data.velocityGradients + assert ratios.shape == (1,) + assert len(gradients) == 1 + assert gradients[0].shape == (dimension, dimension) + assert not hasattr(data, "deformationStates") + + ratios[0] = 2.0 + gradients[0][0, 0] = 3.0 + assert data.volumeRatios[0] == 2.0 + assert data.velocityGradients[0][0, 0] == 3.0 + ratios[0] = 1.0 + gradients[0][0, 0] = 0.0 + + data.TransferFromParticlesToGrid() + data.gridVelocities.Fill(grid_velocity) + data.TransferFromGridToParticles(0.1) + + assert data.volumeRatios[0] == pytest.approx(1.0) + assert np.asarray(data.velocityGradients[0]) == pytest.approx( + np.zeros((dimension, dimension)) + ) From ec11886cdb0460c638f08173e08e30ed6cff72a9 Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Mon, 24 Aug 2026 22:51:22 +0900 Subject: [PATCH 4/8] fix: copy velocity gradient in tests to prevent dangling reference --- Tests/UnitTests/MPMFluidSystemDataTests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/UnitTests/MPMFluidSystemDataTests.cpp b/Tests/UnitTests/MPMFluidSystemDataTests.cpp index 3adb137a59..365f40f4a2 100644 --- a/Tests/UnitTests/MPMFluidSystemDataTests.cpp +++ b/Tests/UnitTests/MPMFluidSystemDataTests.cpp @@ -242,7 +242,7 @@ void ExpectUniformDilation(double rate, double expectedVolumeRatio) data.TransferFromGridToParticles(1.0); - const auto& gradient = data.VelocityGradients()[0]; + const auto gradient = data.VelocityGradients()[0]; for (size_t row = 0; row < N; ++row) { @@ -282,7 +282,7 @@ void ExpectVelocityGradientPreservesComponentOrientation() FillCrossVelocityField(&data); data.TransferFromGridToParticles(0.0); - const auto& gradient = data.VelocityGradients()[0]; + const auto gradient = data.VelocityGradients()[0]; for (size_t row = 0; row < N; ++row) { From d7d8e64f31faebc457e27f377d973e3d39300612 Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Wed, 26 Aug 2026 11:02:54 +0900 Subject: [PATCH 5/8] fix: preserve MPM Python views during transfer --- .../Particle/MPM/MPMFluidSystemData-Impl.hpp | 7 +- .../API/Python/Particle/MPM/MPMSystemData.cpp | 74 ++++++++++++------- Tests/PythonTests/test_mpm_system_data.py | 54 ++++++++++++++ 3 files changed, 107 insertions(+), 28 deletions(-) diff --git a/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp index f8787b746e..6305abea24 100644 --- a/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp +++ b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp @@ -11,6 +11,7 @@ #ifndef CUBBYFLOW_MPM_FLUID_SYSTEM_DATA_IMPL_HPP #define CUBBYFLOW_MPM_FLUID_SYSTEM_DATA_IMPL_HPP +#include #include #include @@ -117,8 +118,10 @@ void MPMFluidSystemData::TransferFromGridToParticles( this->TransferFromGridToParticlesUnchecked(); - m_volumeRatios.Swap(nextVolumeRatios); - m_velocityGradients.Swap(nextVelocityGradients); + std::copy(nextVolumeRatios.begin(), nextVolumeRatios.end(), + m_volumeRatios.begin()); + std::copy(nextVelocityGradients.begin(), nextVelocityGradients.end(), + m_velocityGradients.begin()); } template diff --git a/Sources/API/Python/Particle/MPM/MPMSystemData.cpp b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp index dc34f3de63..de8eb6217f 100644 --- a/Sources/API/Python/Particle/MPM/MPMSystemData.cpp +++ b/Sources/API/Python/Particle/MPM/MPMSystemData.cpp @@ -20,6 +20,22 @@ namespace { namespace py = pybind11; +constexpr char SCALAR_VIEW_DOC[] = R"pbdoc( +Returns a mutable NumPy view into the particle data. + +The view is invalidated when an operation changes the particle count or +replaces the particle state, such as Resize, AddParticle, AddParticles, Set, +or Deserialize. +)pbdoc"; + +constexpr char MATRIX_VIEW_DOC[] = R"pbdoc( +Returns mutable NumPy views into the per-particle matrices. + +The views are invalidated when an operation changes the particle count or +replaces the particle state, such as Resize, AddParticle, AddParticles, Set, +or Deserialize. +)pbdoc"; + template using MPMClass = py::class_, std::shared_ptr>, ParticleSystemData>; @@ -102,38 +118,44 @@ py::list DeformationStates(MPMSystemData& instance) 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()); - }) + cls.def_property_readonly( + "particleMasses", + [](MPMSystemData& instance) { + return ScalarView(instance, instance.ParticleMasses()); + }, + SCALAR_VIEW_DOC) + .def_property_readonly( + "initialVolumes", + [](MPMSystemData& instance) { + return ScalarView(instance, instance.InitialVolumes()); + }, + SCALAR_VIEW_DOC) .def_property_readonly("deformationStates", &DeformationStates); } template void BindParticleState(MPMFluidClass& cls) { - cls.def_property_readonly("particleMasses", - [](MPMFluidSystemData& instance) { - return ScalarView(instance, - instance.ParticleMasses()); - }) - .def_property_readonly("initialVolumes", - [](MPMFluidSystemData& instance) { - return ScalarView(instance, - instance.InitialVolumes()); - }) - .def_property_readonly("volumeRatios", - [](MPMFluidSystemData& instance) { - return ScalarView(instance, - instance.VolumeRatios()); - }) - .def_property_readonly("velocityGradients", &VelocityGradients); + cls.def_property_readonly( + "particleMasses", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, instance.ParticleMasses()); + }, + SCALAR_VIEW_DOC) + .def_property_readonly( + "initialVolumes", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, instance.InitialVolumes()); + }, + SCALAR_VIEW_DOC) + .def_property_readonly( + "volumeRatios", + [](MPMFluidSystemData& instance) { + return ScalarView(instance, instance.VolumeRatios()); + }, + SCALAR_VIEW_DOC) + .def_property_readonly("velocityGradients", &VelocityGradients, + MATRIX_VIEW_DOC); } template diff --git a/Tests/PythonTests/test_mpm_system_data.py b/Tests/PythonTests/test_mpm_system_data.py index 50e58c690a..0663c5ca6b 100644 --- a/Tests/PythonTests/test_mpm_system_data.py +++ b/Tests/PythonTests/test_mpm_system_data.py @@ -118,3 +118,57 @@ def test_mpm_fluid_system_data_api( assert np.asarray(data.velocityGradients[0]) == pytest.approx( np.zeros((dimension, dimension)) ) + + +@pytest.mark.parametrize( + "data_type,resolution,spacing,origin,position,velocity_field,dimension", + [ + ( + pyCubbyFlow.MPMFluidSystemData2, + pyCubbyFlow.Vector2UZ(6, 6), + pyCubbyFlow.Vector2D(1.0, 1.0), + pyCubbyFlow.Vector2D(), + (2.25, 2.25), + lambda pt: (0.2 * pt.x, 0.2 * pt.y), + 2, + ), + ( + pyCubbyFlow.MPMFluidSystemData3, + pyCubbyFlow.Vector3UZ(6, 6, 6), + pyCubbyFlow.Vector3D(1.0, 1.0, 1.0), + pyCubbyFlow.Vector3D(), + (2.25, 2.25, 2.25), + lambda pt: (0.2 * pt.x, 0.2 * pt.y, 0.2 * pt.z), + 3, + ), + ], +) +def test_mpm_fluid_views_remain_valid_after_transfer( + data_type, + resolution, + spacing, + origin, + position, + velocity_field, + dimension, +): + data = data_type(resolution, spacing, origin) + data.AddParticle(position) + data.gridVelocities.Fill(velocity_field) + data.gridVelocitiesBeforeUpdate.Fill(velocity_field) + + ratios = np.asarray(data.volumeRatios) + gradients = data.velocityGradients + ratios[0] = 2.0 + + data.TransferFromGridToParticles(0.5) + + assert ratios[0] == pytest.approx(2.0 * 1.1**dimension) + assert np.asarray(gradients[0]) == pytest.approx( + 0.2 * np.identity(dimension) + ) + + ratios[0] = 3.0 + gradients[0][0, 0] = 4.0 + assert data.volumeRatios[0] == 3.0 + assert data.velocityGradients[0][0, 0] == 4.0 From 4d15b1ba30bc73cd2c9314c2bb5d4b3d86fd6944 Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Wed, 26 Aug 2026 11:03:36 +0900 Subject: [PATCH 6/8] fix: avoid virtual resize in MPM constructor --- Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp index 3232811c66..4fb9ed4ecf 100644 --- a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp +++ b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp @@ -162,10 +162,11 @@ template MPMTransferSystemData::MPMTransferSystemData( const Vector& resolution, const Vector& gridSpacing, const Vector& gridOrigin, size_t numberOfParticles) - : Base{} + : Base{ numberOfParticles }, + m_particleMasses(numberOfParticles, Base::Mass()), + m_initialVolumes(numberOfParticles, 0.0) { ResizeGrid(resolution, gridSpacing, gridOrigin); - Resize(numberOfParticles); } template From 38faa9d831172637fa27b7d5b2fa85ebda6bc1cf Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Wed, 26 Aug 2026 11:03:52 +0900 Subject: [PATCH 7/8] refactor: remove redundant MPM transfer wrapper --- Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp | 6 ------ Includes/Core/Particle/MPM/MPMSystemData.hpp | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp index 4fb9ed4ecf..51be391c95 100644 --- a/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp +++ b/Includes/Core/Particle/MPM/MPMSystemData-Impl.hpp @@ -595,12 +595,6 @@ void MPMSystemData::Set(const ParticleSystemData& other) m_deformationStates.Fill(DeformationState{}); } -template -void MPMSystemData::TransferFromGridToParticles() -{ - TransferBase::TransferFromGridToParticles(); -} - template ConstArrayView1::DeformationState> MPMSystemData::DeformationStates() const diff --git a/Includes/Core/Particle/MPM/MPMSystemData.hpp b/Includes/Core/Particle/MPM/MPMSystemData.hpp index 25b5c4407c..b4c2e85a2e 100644 --- a/Includes/Core/Particle/MPM/MPMSystemData.hpp +++ b/Includes/Core/Particle/MPM/MPMSystemData.hpp @@ -188,6 +188,7 @@ class MPMSystemData final : public MPMTransferSystemData using Base = ParticleSystemData; using TransferBase = MPMTransferSystemData; using DeformationState = SnowDeformationState; + using TransferBase::TransferFromGridToParticles; //! Constructs snow MPM state with a vertex-centered background grid. explicit MPMSystemData( @@ -207,10 +208,6 @@ class MPMSystemData final : public MPMTransferSystemData //! Copies inherited particle state and resets deformation state. void Set(const ParticleSystemData& other) override; - //! Transfers updated background-grid velocities to particles. - //! Stencil nodes outside the finite grid are clamped to its boundary. - void TransferFromGridToParticles(); - //! Returns per-particle deformation states. [[nodiscard]] ConstArrayView1 DeformationStates() const; From 242fe29519924566d7545f5bc291af54df96a06b Mon Sep 17 00:00:00 2001 From: Chris Ohk Date: Wed, 26 Aug 2026 12:31:37 +0900 Subject: [PATCH 8/8] refactor: use std::ranges::copy for particle data transfer --- Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp index 6305abea24..d0bc66b08d 100644 --- a/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp +++ b/Includes/Core/Particle/MPM/MPMFluidSystemData-Impl.hpp @@ -118,10 +118,8 @@ void MPMFluidSystemData::TransferFromGridToParticles( this->TransferFromGridToParticlesUnchecked(); - std::copy(nextVolumeRatios.begin(), nextVolumeRatios.end(), - m_volumeRatios.begin()); - std::copy(nextVelocityGradients.begin(), nextVelocityGradients.end(), - m_velocityGradients.begin()); + std::ranges::copy(nextVolumeRatios, m_volumeRatios.begin()); + std::ranges::copy(nextVelocityGradients, m_velocityGradients.begin()); } template