Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion genesis/engine/entities/particle_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,11 @@ def sample(self):
sampler=sampler,
)

particles_i += np.asarray(morph_i.pos, dtype=gs.np_float)
particles_i = gu.transform_by_trans_quat(
particles_i,
np.asarray(morph_i.pos, dtype=gs.np_float),
np.asarray(morph_i.quat, dtype=gs.np_float),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transform is mathematically correct, but please compute pos_i and quat_i once and use the same values for both the particles and self._vmesh[i] in this loop, then remove the later visual-transform loop. This avoids maintaining two derivations of the same member pose and keeps particle placement, rendering, and skinning in the same frame.

These are validated internal tuples, so the repository guidelines also require np.array rather than np.asarray.

pos_i = np.array(morph_i.pos, dtype=gs.np_float)
quat_i = np.array(morph_i.quat, dtype=gs.np_float)
particles.append(gu.transform_by_trans_quat(particles_i, pos_i, quat_i))
self._vmesh[i].apply_transform(gu.trans_quat_to_T(pos_i, quat_i))

particles.append(particles_i)
elif isinstance(self._morph, (gs.options.morphs.Primitive, gs.options.morphs.Mesh)):
particles = self._vmesh.particlize(self._particle_size, self.sampler)
Expand Down
28 changes: 28 additions & 0 deletions tests/particles/test_mpm.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import numpy as np
import pytest
import torch
import trimesh

import genesis as gs


@pytest.mark.required
def test_mesh_set_particle_transform(show_viewer):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CODING_GUIDELINES.md sections 12.1–12.2 require bug-fix regression assertions to be added to the existing test covering the capability; bug fixes are explicitly not an exception.

Please fold this MeshSet entity and its assertions into an existing MPM test and reuse that scene build. A separate test incurs another full Genesis initialization for every CI configuration.

pos = np.array((0.2, 0.1, 0.2))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this value as a tuple:

pos = (0.2, 0.1, 0.2)

The scene-construction guidelines prohibit wrapping controlled array-like inputs in np.array, and the project assertion helper accepts tuples directly.

mesh = trimesh.creation.box(extents=(0.04, 0.12, 0.04))
scene = gs.Scene(
mpm_options=gs.options.MPMOptions(
lower_bound=(-0.5, -0.5, -0.5),
upper_bound=(0.5, 0.5, 0.5),
particle_size=0.01,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keyword arguments must follow the callee's declaration order, so particle_size must precede lower_bound and upper_bound.

The custom bounds are also unnecessary for this geometry because it already fits inside the default MPM domain. This can be reduced to:

mpm_options=gs.options.MPMOptions(
    particle_size=0.01,
),

),
show_viewer=show_viewer,
)
entity = scene.add_entity(
morph=gs.morphs.MeshSet(
files=(mesh,),
poss=(pos,),
eulers=((0.0, 0.0, 90.0),),
),
material=gs.materials.MPM.Elastic(sampler="regular"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scene-building calls require one option per line, even when only one option is provided:

material=gs.materials.MPM.Elastic(
    sampler="regular",
),

)

particles = entity.init_particles
np.testing.assert_allclose(particles.mean(axis=0), pos, atol=1e-6)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the repository assertion utility instead of NumPy's testing API:

from ..utils.assertions import assert_allclose

Then call assert_allclose(..., atol=1e-6).

An explicit atol=1e-6 sits at roughly 5x the measured fp32 floor (~2e-7) while still detecting the 0.08 failure by five orders of magnitude, and stays a fixed absolute bound across precision arms.

np.testing.assert_allclose(np.ptp(particles, axis=0), (0.11, 0.03, 0.03), atol=1e-6)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches a completely missing rotation, but it does not fully validate MeshSet pose semantics.

A single centered box cannot detect applying the wrong member's pose, and +90° and −90° rotations have identical means and extents. Please test at least two members with different rotation axes and make one mesh off-center or assert another direction-sensitive quantity so an inverse rotation fails.

Please also derive the expected spans from extents - particle_size and consider asserting that the particle and public visual-mesh AABBs remain aligned.

Note this interacts with the bounds comment above: keep every member inside the default MPM domain (z well above 0, within the safety padding), or the custom bounds become necessary again.



@pytest.mark.required
def test_particle_constraints(show_viewer):
scene = gs.Scene(
Expand Down