Skip to content

Commit 9479f5a

Browse files
authored
Fix SolverVBD ignoring live stiffness/damping updates (#4031)
1 parent 3e57f00 commit 9479f5a

4 files changed

Lines changed: 582 additions & 10 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix `SolverVBD.notify_model_changed(JOINT_DOF_PROPERTIES)` to refresh rod stretch/shear/bend/twist material stiffness and damping, and the derived per-body structural summary used for contact conditioning, which were frozen at construction so live `joint_target_ke`/`joint_target_kd` edits were silently ignored. Also refreshes the legacy AVBD penalty cache for REVOLUTE/PRISMATIC/D6 drive/limit slots; compliant ALM already gathered those coefficients live.

newton/_src/solvers/vbd/rigid_vbd_kernels.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3813,6 +3813,87 @@ def refresh_body_structural_k(
38133813
wp.atomic_max(body_structural_k, child_id, k_linear)
38143814

38153815

3816+
@wp.kernel
3817+
def refresh_joint_material_params(
3818+
joint_type: wp.array[int],
3819+
joint_qd_start: wp.array[int],
3820+
joint_dof_dim: wp.array2d[int],
3821+
joint_constraint_start: wp.array[wp.int32],
3822+
joint_target_ke: wp.array[float],
3823+
joint_target_kd: wp.array[float],
3824+
joint_limit_ke: wp.array[float],
3825+
legacy_lin_k_start: float,
3826+
legacy_ang_k_start: float,
3827+
joint_material_k: wp.array[float],
3828+
joint_penalty_k: wp.array[float],
3829+
joint_penalty_k_min: wp.array[float],
3830+
joint_penalty_kd: wp.array[float],
3831+
):
3832+
"""Recompute ``joint_material_k`` from ``joint_target_ke``/``joint_limit_ke`` and reseed
3833+
``joint_penalty_k``/``joint_penalty_k_min`` to match, plus ROD ``joint_penalty_kd`` from
3834+
``joint_target_kd`` (see ``_init_joint_penalty_k`` for the same formulas at construction
3835+
time).
3836+
3837+
Stiffness slots are reseeded only where the effective stiffness actually changed, so a
3838+
slot the caller did not touch keeps whatever legacy AVBD ramping has accumulated in
3839+
``joint_penalty_k``. Damping is written unconditionally: it has no ramp state.
3840+
3841+
Stiffness covers ROD (all four material slots) and the drive/limit slot(s) of REVOLUTE,
3842+
PRISMATIC, and D6. Only legacy AVBD reads those non-ROD slots; compliant ALM gathers the
3843+
same coefficients live from the model each solve (see ``_load_joint_axis_drive_limit``).
3844+
Not covered: BALL, FIXED, and REVOLUTE/PRISMATIC/D6's structural slots, which come from the
3845+
solver-wide ``rigid_joint_linear_ke``/``rigid_joint_angular_ke`` constants rather than
3846+
``joint_target_ke``.
3847+
3848+
Damping is ROD-only: other joint types' ``joint_penalty_kd`` slots hold the solver-wide
3849+
``rigid_joint_{linear,angular}_kd`` constants or zero, and their drive damping is read
3850+
live from ``joint_target_kd`` by the stepping kernels rather than cached here.
3851+
3852+
Args:
3853+
legacy_lin_k_start: Ramp-cap seed for linear slots [N/m], negative to disable.
3854+
legacy_ang_k_start: Ramp-cap seed for angular slots [N·m/rad], negative to disable.
3855+
"""
3856+
joint_id = wp.tid()
3857+
jt = joint_type[joint_id]
3858+
c0 = joint_constraint_start[joint_id]
3859+
dof0 = joint_qd_start[joint_id]
3860+
3861+
if jt == JointType.ROD:
3862+
for s in range(4): # 0=stretch, 1=shear, 2=bend, 3=twist
3863+
ke = joint_target_ke[dof0 + s]
3864+
if joint_material_k[c0 + s] != ke:
3865+
joint_material_k[c0 + s] = ke
3866+
seed = legacy_lin_k_start if s < 2 else legacy_ang_k_start
3867+
seeded = wp.min(seed, ke) if seed >= 0.0 else ke
3868+
joint_penalty_k[c0 + s] = seeded
3869+
joint_penalty_k_min[c0 + s] = seeded
3870+
joint_penalty_kd[c0 + s] = joint_target_kd[dof0 + s]
3871+
return
3872+
3873+
linear_count = int(0)
3874+
angular_count = int(0)
3875+
if jt == JointType.PRISMATIC:
3876+
linear_count = 1
3877+
elif jt == JointType.REVOLUTE:
3878+
angular_count = 1
3879+
elif jt == JointType.D6:
3880+
linear_count = joint_dof_dim[joint_id, 0]
3881+
angular_count = joint_dof_dim[joint_id, 1]
3882+
else:
3883+
return # BALL, FIXED, and anything else: no joint_target_ke-derived slot.
3884+
3885+
slot0 = c0 + 2 # drive/limit slots follow the 2 structural slots (see _init_joint_penalty_k)
3886+
for axis in range(linear_count + angular_count):
3887+
dof = dof0 + axis
3888+
ke = wp.max(joint_target_ke[dof], joint_limit_ke[dof])
3889+
if joint_material_k[slot0 + axis] != ke:
3890+
seed = legacy_lin_k_start if axis < linear_count else legacy_ang_k_start
3891+
seeded = wp.min(seed, ke) if seed >= 0.0 else ke
3892+
joint_material_k[slot0 + axis] = ke
3893+
joint_penalty_k[slot0 + axis] = seeded
3894+
joint_penalty_k_min[slot0 + axis] = seeded
3895+
3896+
38163897
# -----------------------------
38173898
# Pre-iteration kernels (once per step)
38183899
# -----------------------------

newton/_src/solvers/vbd/solver_vbd.py

Lines changed: 73 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
init_body_particle_contacts,
6767
init_rod_rest_bend_twist,
6868
refresh_body_structural_k,
69+
refresh_joint_material_params,
6970
reset_rigid_state,
7071
snapshot_body_body_contact_history,
7172
solve_rigid_body,
@@ -157,13 +158,19 @@ class SolverVBD(SolverBase, CouplingInterface):
157158
is read live. After changing enable flags, call
158159
:meth:`notify_model_changed` with
159160
:attr:`~newton.ModelFlags.JOINT_PROPERTIES` to refresh derived contact
160-
conditioning. Structural-slot material, constraint layout, and rest-angle
161-
offsets are captured at construction; rebuild ``SolverVBD`` after changing
162-
them.
161+
conditioning. Structural-slot material (``rigid_joint_linear_ke``/
162+
``rigid_joint_angular_ke``), constraint layout, and rest-angle offsets are
163+
captured at construction; rebuild ``SolverVBD`` after changing them.
163164
- :attr:`~newton.Model.joint_target_ke`/:attr:`~newton.Model.joint_target_kd` are supported
164165
for REVOLUTE, PRISMATIC, D6 (as drives), and ROD (as stretch, shear,
165166
bend, and twist stiffness and damping).
166-
VBD interprets ``kd`` as absolute damping in physical units.
167+
VBD interprets ``kd`` as absolute damping in physical units. ROD values are cached in
168+
solver-owned buffers at construction; after editing them call
169+
:meth:`notify_model_changed` with
170+
:attr:`~newton.ModelFlags.JOINT_DOF_PROPERTIES` to refresh that cache. REVOLUTE,
171+
PRISMATIC, and D6 drive/limit coefficients are gathered live from the model on every
172+
solve and need no notification -- except on the deprecated legacy AVBD path, whose
173+
cached penalty state the same flag refreshes.
167174
- :attr:`~newton.Model.joint_limit_lower`/:attr:`~newton.Model.joint_limit_upper` and
168175
:attr:`~newton.Model.joint_limit_ke`/:attr:`~newton.Model.joint_limit_kd` are supported
169176
for REVOLUTE, PRISMATIC, and D6 joints.
@@ -1024,10 +1031,19 @@ def _init_rigid_system(
10241031
def notify_model_changed(self, flags: ModelFlags | int) -> None:
10251032
self._apply_module_options()
10261033
refresh_structural_k = (
1027-
bool(flags & ModelFlags.JOINT_PROPERTIES) and self._integrates_rigid_bodies and self.model.joint_count > 0
1034+
bool(flags & (ModelFlags.JOINT_PROPERTIES | ModelFlags.JOINT_DOF_PROPERTIES))
1035+
and self._integrates_rigid_bodies
1036+
and self.model.joint_count > 0
10281037
)
10291038
if flags & (ModelFlags.BODY_PROPERTIES | ModelFlags.BODY_INERTIAL_PROPERTIES):
10301039
self._refresh_kinematic_state()
1040+
if flags & ModelFlags.JOINT_DOF_PROPERTIES and self._integrates_rigid_bodies and self.model.joint_count > 0:
1041+
if self.rigid_compliant_alm:
1042+
self._validate_compliant_joint_dof_materials()
1043+
# Must run before _refresh_structural_k() below: that summary reads
1044+
# joint_material_k as an input, so a stale material_k here would
1045+
# produce a stale body_structural_k that is harder to spot.
1046+
self._refresh_joint_material_params()
10311047
if refresh_structural_k:
10321048
self._refresh_structural_k()
10331049
if flags & (ModelFlags.JOINT_PROPERTIES | ModelFlags.BODY_PROPERTIES):
@@ -1484,8 +1500,10 @@ def _init_joint_penalty_k(self):
14841500
(joint_penalty_k, joint_penalty_k_min, joint_material_k, joint_rho,
14851501
joint_penalty_kd, joint_is_hard) tuple:
14861502
- joint_penalty_k: mutable legacy solver penalty per constraint scalar.
1487-
- joint_penalty_k_min: frozen floor for the mutable legacy solver penalty.
1488-
- joint_material_k: frozen material stiffness (= slot-specific ke).
1503+
- joint_penalty_k_min: floor for the mutable legacy solver penalty; refreshed
1504+
by ``notify_model_changed(JOINT_DOF_PROPERTIES)``.
1505+
- joint_material_k: material stiffness (= slot-specific ke); refreshed
1506+
by ``notify_model_changed(JOINT_DOF_PROPERTIES)``.
14891507
- joint_rho: zeroed solver-owned storage; compliant ALM fills
14901508
structural slots automatically each step.
14911509
- joint_penalty_kd: damping coefficient per constraint scalar.
@@ -1702,14 +1720,19 @@ def _init_structural_k(self) -> None:
17021720
own summary before combining endpoints.
17031721
Direction- and chain-blind by design: it bounds neighborhood stiffness to
17041722
condition rho and never enters a force law.
1705-
Structural material and topology are construction-time state. The summary
1706-
is refreshed in place after a notified joint-enable change.
1723+
Topology and the non-rod structural constants are construction-time state. The summary
1724+
is refreshed in place after a notified joint-enable change, and after a
1725+
``JOINT_DOF_PROPERTIES`` change to rod stretch/shear stiffness, which feeds it.
17071726
"""
17081727
self.body_structural_k = wp.empty(self.model.body_count, dtype=float, device=self.device)
17091728
self._refresh_structural_k()
17101729

17111730
def _refresh_structural_k(self) -> None:
1712-
"""Refresh the enable-dependent structural summary without reallocating it."""
1731+
"""Refresh the structural summary in place, without reallocating it.
1732+
1733+
Depends on joint-enable flags and on rod stretch/shear ``joint_material_k``, so it is
1734+
rerun for both ``JOINT_PROPERTIES`` and ``JOINT_DOF_PROPERTIES``.
1735+
"""
17131736
self.body_structural_k.zero_()
17141737
if self.model.joint_count == 0:
17151738
return
@@ -1729,6 +1752,46 @@ def _refresh_structural_k(self) -> None:
17291752
device=self.device,
17301753
)
17311754

1755+
def _refresh_joint_material_params(self) -> None:
1756+
"""Refresh ``joint_target_ke``/``joint_target_kd``-derived material stiffness and damping.
1757+
1758+
Stiffness covers ROD and the drive/limit slot(s) of REVOLUTE, PRISMATIC, and D6; its
1759+
penalty and floor are reseeded alongside. Only legacy AVBD reads those non-ROD slots --
1760+
compliant ALM gathers the same coefficients live from the model each solve. Not covered:
1761+
BALL, FIXED, and REVOLUTE/PRISMATIC/D6's structural slots, which come from the
1762+
solver-wide ``rigid_joint_linear_ke``/``rigid_joint_angular_ke`` constants.
1763+
1764+
Damping is ROD-only -- other joint types read drive damping live from
1765+
``joint_target_kd`` at step time rather than caching it here.
1766+
1767+
Stiffness slots are reseeded only where the effective stiffness changed, so untouched
1768+
slots keep any legacy AVBD ramp they have accumulated.
1769+
"""
1770+
if self.model.joint_count == 0:
1771+
return
1772+
wp.launch(
1773+
kernel=refresh_joint_material_params,
1774+
dim=self.model.joint_count,
1775+
inputs=[
1776+
self.model.joint_type,
1777+
self.model.joint_qd_start,
1778+
self.model.joint_dof_dim,
1779+
self.joint_constraint_start,
1780+
self.model.joint_target_ke,
1781+
self.model.joint_target_kd,
1782+
self.model.joint_limit_ke,
1783+
self.rigid_joint_linear_k_start if self.rigid_joint_linear_k_start is not None else -1.0,
1784+
self.rigid_joint_angular_k_start if self.rigid_joint_angular_k_start is not None else -1.0,
1785+
],
1786+
outputs=[
1787+
self.joint_material_k,
1788+
self.joint_penalty_k,
1789+
self.joint_penalty_k_min,
1790+
self.joint_penalty_kd,
1791+
],
1792+
device=self.device,
1793+
)
1794+
17321795
def _init_joint_rest_angle(self):
17331796
"""Compute per-DOF rest-pose joint angles from ``model.joint_q``.
17341797

0 commit comments

Comments
 (0)