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