genesis.engine.coupler のソースコード

import numpy as np
import taichi as ti

import genesis as gs
from genesis.repr_base import RBC


[ドキュメント]@ti.data_oriented class Coupler(RBC): """ This class handles all the coupling between different solvers. """ # ------------------------------------------------------------------------------------ # --------------------------------- Initialization ----------------------------------- # ------------------------------------------------------------------------------------ def __init__( self, simulator, options, ): self.sim = simulator self.options = options self.tool_solver = self.sim.tool_solver self.rigid_solver = self.sim.rigid_solver self.avatar_solver = self.sim.avatar_solver self.mpm_solver = self.sim.mpm_solver self.sph_solver = self.sim.sph_solver self.pbd_solver = self.sim.pbd_solver self.fem_solver = self.sim.fem_solver self.sf_solver = self.sim.sf_solver
[ドキュメント] def build(self): self._rigid_mpm = self.rigid_solver.is_active() and self.mpm_solver.is_active() and self.options.rigid_mpm self._rigid_sph = self.rigid_solver.is_active() and self.sph_solver.is_active() and self.options.rigid_sph self._rigid_pbd = self.rigid_solver.is_active() and self.pbd_solver.is_active() and self.options.rigid_pbd self._rigid_fem = self.rigid_solver.is_active() and self.fem_solver.is_active() and self.options.rigid_fem self._mpm_sph = self.mpm_solver.is_active() and self.sph_solver.is_active() and self.options.mpm_sph self._mpm_pbd = self.mpm_solver.is_active() and self.pbd_solver.is_active() and self.options.mpm_pbd self._fem_mpm = self.fem_solver.is_active() and self.mpm_solver.is_active() and self.options.fem_mpm self._fem_sph = self.fem_solver.is_active() and self.sph_solver.is_active() and self.options.fem_sph if self._rigid_mpm and self.mpm_solver.enable_CPIC: # this field stores the geom index of the thin shell rigid object (if any) that separates particle and its surrounding grid cell self.cpic_flag = ti.field(gs.ti_int, shape=(self.mpm_solver.n_particles, 3, 3, 3)) self.mpm_rigid_normal = ti.Vector.field( 3, dtype=gs.ti_float, shape=(self.mpm_solver.n_particles, self.rigid_solver.n_geoms_) ) if self._rigid_sph: self.sph_rigid_normal = ti.Vector.field( 3, dtype=gs.ti_float, shape=(self.sph_solver.n_particles, self.rigid_solver.n_geoms_) ) self.sph_rigid_normal_reordered = ti.Vector.field( 3, dtype=gs.ti_float, shape=(self.sph_solver.n_particles, self.rigid_solver.n_geoms_) ) if self._rigid_pbd: self.pbd_rigid_normal = ti.Vector.field( 3, dtype=gs.ti_float, shape=(self.pbd_solver.n_particles, self.rigid_solver.n_geoms) ) self.pbd_rigid_normal_reordered = ti.Vector.field( 3, dtype=gs.ti_float, shape=(self.pbd_solver.n_particles, self.rigid_solver.n_geoms) ) if self._mpm_sph: self.mpm_sph_stencil_size = int(np.floor(self.mpm_solver.dx / self.sph_solver.hash_grid_cell_size) + 2) if self._mpm_pbd: self.mpm_pbd_stencil_size = int(np.floor(self.mpm_solver.dx / self.pbd_solver.hash_grid_cell_size) + 2) ## DEBUG self._dx = 1 / 1024 self._stencil_size = int(np.floor(self._dx / self.sph_solver.hash_grid_cell_size) + 2) self.reset()
[ドキュメント] def reset(self): if self._rigid_mpm and self.mpm_solver.enable_CPIC: self.mpm_rigid_normal.fill(0) if self._rigid_sph: self.sph_rigid_normal.fill(0)
@ti.func def _func_collide_with_rigid(self, f, pos_world, vel, mass): for i_g in range(self.rigid_solver.n_geoms): if self.rigid_solver.geoms_info[i_g].needs_coup: i_b = 0 vel = self._func_collide_with_rigid_geom(pos_world, vel, mass, i_g, i_b) return vel @ti.func def _func_collide_with_rigid_geom(self, pos_world, vel, mass, geom_idx, batch_idx): g_info = self.rigid_solver.geoms_info[geom_idx] signed_dist = self.rigid_solver.sdf.sdf_world(pos_world, geom_idx, batch_idx) # bigger coup_softness implies that the coupling influence extends further away from the object. influence = ti.min(ti.exp(-signed_dist / max(1e-10, g_info.coup_softness)), 1) if influence > 0.1: normal_rigid = self.rigid_solver.sdf.sdf_normal_world(pos_world, geom_idx, batch_idx) vel = self._func_collide_in_rigid_geom(pos_world, vel, mass, normal_rigid, influence, geom_idx, batch_idx) return vel @ti.func def _func_collide_with_rigid_geom_robust(self, pos_world, vel, mass, normal_prev, geom_idx, batch_idx): """ Similar to _func_collide_with_rigid_geom, but additionally handles potential side flip due to penetration. """ g_info = self.rigid_solver.geoms_info[geom_idx] signed_dist = self.rigid_solver.sdf.sdf_world(pos_world, geom_idx, batch_idx) normal_rigid = self.rigid_solver.sdf.sdf_normal_world(pos_world, geom_idx, batch_idx) # bigger coup_softness implies that the coupling influence extends further away from the object. influence = ti.min(ti.exp(-signed_dist / max(1e-10, g_info.coup_softness)), 1) # if normal_rigid.dot(normal_prev) < 0: # side flip due to penetration # influence = 1.0 # normal_rigid = normal_prev if influence > 0.1: vel = self._func_collide_in_rigid_geom(pos_world, vel, mass, normal_rigid, influence, geom_idx, batch_idx) # attraction force # if 0.001 < signed_dist < 0.01: # vel = vel - normal_rigid * 0.1 * signed_dist return vel, normal_rigid @ti.func def _func_collide_in_rigid_geom(self, pos_world, vel, mass, normal_rigid, influence, geom_idx, batch_idx): """ Resolves collision when a particle is already in collision with a rigid object. This function assumes known normal_rigid and influence. """ g_info = self.rigid_solver.geoms_info[geom_idx] vel_rigid = self.rigid_solver._func_vel_at_point(pos_world, g_info.link_idx, batch_idx) # v w.r.t rigid rvel = vel - vel_rigid rvel_normal_magnitude = rvel.dot(normal_rigid) # negative if inward if rvel_normal_magnitude < 0: # colliding #################### rigid -> particle #################### # tangential component rvel_tan = rvel - rvel_normal_magnitude * normal_rigid rvel_tan_norm = rvel_tan.norm(gs.EPS) # tangential component after friction rvel_tan = ( rvel_tan / rvel_tan_norm * ti.max(0, rvel_tan_norm + rvel_normal_magnitude * g_info.coup_friction) ) # normal component after collision rvel_normal = -normal_rigid * rvel_normal_magnitude * g_info.coup_restitution # normal + tangential component rvel_new = rvel_tan + rvel_normal # apply influence vel_old = vel vel = vel_rigid + rvel_new * influence + rvel * (1 - influence) #################### particle -> rigid #################### # Compute delta momentum and apply to rigid body. delta_mv = mass * (vel - vel_old) force = -delta_mv / self.rigid_solver.substep_dt self.rigid_solver._func_apply_external_force(pos_world, force, g_info.link_idx, batch_idx) return vel @ti.func def _func_mpm_tool(self, f, pos_world, vel): for entity in ti.static(self.tool_solver.entities): if ti.static(entity.material.collision): vel = entity.collide(f, pos_world, vel) return vel
[ドキュメント] @ti.kernel def mpm_grid_op(self, f: ti.i32, t: ti.f32): """ This combines mpm's grid_op with coupling operations. If we decouple grid_op with coupling with different solvers, we need to run grid-level operations for each coupling pair, which is inefficient. """ for I in ti.grouped(ti.ndrange(*self.mpm_solver.grid_res)): if self.mpm_solver.grid[f, I].mass > gs.EPS: #################### MPM grid op #################### # Momentum to velocity vel_mpm = (1 / self.mpm_solver.grid[f, I].mass) * self.mpm_solver.grid[f, I].vel_in # gravity vel_mpm += self.mpm_solver.substep_dt * self.mpm_solver._gravity[None] pos = (I + self.mpm_solver.grid_offset) * self.mpm_solver.dx mass_mpm = self.mpm_solver.grid[f, I].mass / self.mpm_solver._p_vol_scale # external force fields for i_ff in ti.static(range(len(self.mpm_solver._ffs))): vel_mpm += self.mpm_solver._ffs[i_ff].get_acc(pos, vel_mpm, t) * self.mpm_solver.substep_dt #################### MPM <-> Tool #################### if ti.static(self.tool_solver.is_active()): vel_mpm = self._func_mpm_tool(f, pos, vel_mpm) #################### MPM <-> Rigid #################### if ti.static(self._rigid_mpm): vel_mpm = self._func_collide_with_rigid( f, pos, vel_mpm, mass_mpm, ) #################### MPM <-> SPH #################### if ti.static(self._mpm_sph): # using the lower corner of MPM cell to find the corresponding SPH base cell base = self.sph_solver.sh.pos_to_grid(pos - 0.5 * self.mpm_solver.dx) # ---------- SPH -> MPM ---------- sph_vel = ti.Vector([0.0, 0.0, 0.0]) colliding_particles = 0 for offset in ti.grouped( ti.ndrange(self.mpm_sph_stencil_size, self.mpm_sph_stencil_size, self.mpm_sph_stencil_size) ): slot_idx = self.sph_solver.sh.grid_to_slot(base + offset) for i in range( self.sph_solver.sh.slot_start[slot_idx], self.sph_solver.sh.slot_start[slot_idx] + self.sph_solver.sh.slot_size[slot_idx], ): if ( ti.abs(pos - self.sph_solver.particles_reordered.pos[i]).max() < self.mpm_solver.dx * 0.5 ): sph_vel += self.sph_solver.particles_reordered.vel[i] colliding_particles += 1 if colliding_particles > 0: vel_old = vel_mpm vel_mpm = sph_vel / colliding_particles # ---------- MPM -> SPH ---------- delta_mv = mass_mpm * (vel_mpm - vel_old) for offset in ti.grouped( ti.ndrange(self.mpm_sph_stencil_size, self.mpm_sph_stencil_size, self.mpm_sph_stencil_size) ): slot_idx = self.sph_solver.sh.grid_to_slot(base + offset) for i in range( self.sph_solver.sh.slot_start[slot_idx], self.sph_solver.sh.slot_start[slot_idx] + self.sph_solver.sh.slot_size[slot_idx], ): if ( ti.abs(pos - self.sph_solver.particles_reordered.pos[i]).max() < self.mpm_solver.dx * 0.5 ): self.sph_solver.particles_reordered[i].vel = ( self.sph_solver.particles_reordered[i].vel - delta_mv / self.sph_solver.particles_info_reordered[i].mass ) #################### MPM <-> PBD #################### if ti.static(self._mpm_pbd): # using the lower corner of MPM cell to find the corresponding PBD base cell base = self.pbd_solver.sh.pos_to_grid(pos - 0.5 * self.mpm_solver.dx) # ---------- PBD -> MPM ---------- pbd_vel = ti.Vector([0.0, 0.0, 0.0]) colliding_particles = 0 for offset in ti.grouped( ti.ndrange(self.mpm_pbd_stencil_size, self.mpm_pbd_stencil_size, self.mpm_pbd_stencil_size) ): slot_idx = self.pbd_solver.sh.grid_to_slot(base + offset) for i in range( self.pbd_solver.sh.slot_start[slot_idx], self.pbd_solver.sh.slot_start[slot_idx] + self.pbd_solver.sh.slot_size[slot_idx], ): if ( ti.abs(pos - self.pbd_solver.particles_reordered.pos[i]).max() < self.mpm_solver.dx * 0.5 ): pbd_vel += self.pbd_solver.particles_reordered.vel[i] colliding_particles += 1 if colliding_particles > 0: vel_old = vel_mpm vel_mpm = pbd_vel / colliding_particles # ---------- MPM -> PBD ---------- delta_mv = mass_mpm * (vel_mpm - vel_old) for offset in ti.grouped( ti.ndrange(self.mpm_pbd_stencil_size, self.mpm_pbd_stencil_size, self.mpm_pbd_stencil_size) ): slot_idx = self.pbd_solver.sh.grid_to_slot(base + offset) for i in range( self.pbd_solver.sh.slot_start[slot_idx], self.pbd_solver.sh.slot_start[slot_idx] + self.pbd_solver.sh.slot_size[slot_idx], ): if ( ti.abs(pos - self.pbd_solver.particles_reordered.pos[i]).max() < self.mpm_solver.dx * 0.5 ): if self.pbd_solver.particles_reordered[i].free: self.pbd_solver.particles_reordered[i].vel = ( self.pbd_solver.particles_reordered[i].vel - delta_mv / self.pbd_solver.particles_info_reordered[i].mass ) #################### MPM boundary #################### _, self.mpm_solver.grid[f, I].vel_out = self.mpm_solver.boundary.impose_pos_vel(pos, vel_mpm)
[ドキュメント] @ti.kernel def mpm_surface_to_particle(self, f: ti.i32): for i in range(self.mpm_solver.n_particles): if self.mpm_solver.particles_ng[f, i].active: for i_g in range(self.rigid_solver.n_geoms): if self.rigid_solver.geoms_info[i_g].needs_coup: sdf_normal = self.rigid_solver.sdf.sdf_normal_world(self.mpm_solver.particles[f, i].pos, i_g, 0) # we only update the normal if the particle does not the object if sdf_normal.dot(self.mpm_rigid_normal[i, i_g]) >= 0: self.mpm_rigid_normal[i, i_g] = sdf_normal
[ドキュメント] @ti.kernel def fem_surface_force(self, f: ti.i32): # TODO: all collisions are on vertices instead of surface and edge for i in range(self.fem_solver.n_surfaces): if self.fem_solver.surface[i].active: dt = self.fem_solver.substep_dt iel = self.fem_solver.surface[i].tri2el mass = self.fem_solver.elements_i[iel].mass_scaled / self.fem_solver.vol_scale p1 = self.fem_solver.elements_v[f, self.fem_solver.surface[i].tri2v[0]].pos p2 = self.fem_solver.elements_v[f, self.fem_solver.surface[i].tri2v[1]].pos p3 = self.fem_solver.elements_v[f, self.fem_solver.surface[i].tri2v[2]].pos u = p2 - p1 v = p3 - p1 surface_normal = ti.math.cross(u, v) surface_normal = surface_normal / surface_normal.norm(gs.EPS) # FEM <-> Rigid if ti.static(self._rigid_fem): # NOTE: collision only on surface vertices for j in ti.static(range(3)): iv = self.fem_solver.surface[i].tri2v[j] vel_fem_sv = self._func_collide_with_rigid( f, self.fem_solver.elements_v[f, iv].pos, self.fem_solver.elements_v[f + 1, iv].vel, mass / 3.0, # assume element mass uniformly distributed to vertices ) self.fem_solver.elements_v[f + 1, iv].vel = vel_fem_sv # FEM <-> MPM (interact with MPM grid instead of particles) # NOTE: not doing this in mpm_grid_op otherwise we need to search for fem surface for each particles # however, this function is called after mpm boundary conditions. if ti.static(self._fem_mpm): for j in ti.static(range(3)): iv = self.fem_solver.surface[i].tri2v[j] pos = self.fem_solver.elements_v[f, iv].pos vel_fem_sv = self.fem_solver.elements_v[f + 1, iv].vel mass_fem_sv = mass / 4.0 # assume element mass uniformly distributed # follow MPM p2g scheme vel_mpm = ti.Vector([0.0, 0.0, 0.0]) mass_mpm = 0.0 mpm_base = ti.floor(pos * self.mpm_solver.inv_dx - 0.5).cast(gs.ti_int) mpm_fx = pos * self.mpm_solver.inv_dx - mpm_base.cast(gs.ti_float) mpm_w = [0.5 * (1.5 - mpm_fx) ** 2, 0.75 - (mpm_fx - 1.0) ** 2, 0.5 * (mpm_fx - 0.5) ** 2] new_vel_fem_sv = vel_fem_sv for mpm_offset in ti.static(ti.grouped(self.mpm_solver.stencil_range())): mpm_grid_I = mpm_base - self.mpm_solver.grid_offset + mpm_offset mpm_grid_mass = self.mpm_solver.grid[f, mpm_grid_I].mass / self.mpm_solver.p_vol_scale mpm_weight = ti.cast(1.0, gs.ti_float) for d in ti.static(range(3)): mpm_weight *= mpm_w[mpm_offset[d]][d] # FEM -> MPM mpm_grid_pos = (mpm_grid_I + self.mpm_solver.grid_offset) * self.mpm_solver.dx signed_dist = (mpm_grid_pos - pos).dot(surface_normal) if signed_dist <= self.mpm_solver.dx: # NOTE: use dx as minimal unit for collision vel_mpm_at_cell = mpm_weight * self.mpm_solver.grid[f, mpm_grid_I].vel_out mass_mpm_at_cell = mpm_weight * mpm_grid_mass vel_mpm += vel_mpm_at_cell mass_mpm += mass_mpm_at_cell if mass_mpm_at_cell > gs.EPS: delta_mpm_vel_at_cell_unmul = ( vel_fem_sv * mpm_weight - self.mpm_solver.grid[f, mpm_grid_I].vel_out ) mass_mul_at_cell = ( mpm_grid_mass / mass_fem_sv ) # NOTE: use un-reweighted mass instead of mass_mpm_at_cell delta_mpm_vel_at_cell = delta_mpm_vel_at_cell_unmul * mass_mul_at_cell self.mpm_solver.grid[f, mpm_grid_I].vel_out += delta_mpm_vel_at_cell new_vel_fem_sv -= delta_mpm_vel_at_cell * mass_mpm_at_cell / mass_fem_sv # MPM -> FEM if mass_mpm > gs.EPS: # delta_mv = (vel_mpm - vel_fem_sv) * mass_mpm # delta_vel_fem_sv = delta_mv / mass_fem_sv # self.fem_solver.elements_v[f + 1, iv].vel += delta_vel_fem_sv self.fem_solver.elements_v[f + 1, iv].vel = new_vel_fem_sv # FEM <-> SPH TODO: this doesn't work well if ti.static(self._fem_sph): for j in ti.static(range(3)): iv = self.fem_solver.surface[i].tri2v[j] pos = self.fem_solver.elements_v[f, iv].pos vel_fem_sv = self.fem_solver.elements_v[f + 1, iv].vel mass_fem_sv = mass / 4.0 dx = self.sph_solver.hash_grid_cell_size # self._dx stencil_size = 2 # self._stencil_size base = self.sph_solver.sh.pos_to_grid(pos - 0.5 * dx) # ---------- SPH -> FEM ---------- sph_vel = ti.Vector([0.0, 0.0, 0.0]) colliding_particles = 0 for offset in ti.grouped(ti.ndrange(stencil_size, stencil_size, stencil_size)): slot_idx = self.sph_solver.sh.grid_to_slot(base + offset) for k in range( self.sph_solver.sh.slot_start[slot_idx], self.sph_solver.sh.slot_start[slot_idx] + self.sph_solver.sh.slot_size[slot_idx], ): if ti.abs(pos - self.sph_solver.particles_reordered.pos[k]).max() < dx * 0.5: sph_vel += self.sph_solver.particles_reordered.vel[k] colliding_particles += 1 if colliding_particles > 0: vel_old = vel_fem_sv vel_fem_sv_unprojected = sph_vel / colliding_particles vel_fem_sv = ( vel_fem_sv_unprojected.dot(surface_normal) * surface_normal ) # exclude tangential velocity # ---------- FEM -> SPH ---------- delta_mv = mass_fem_sv * (vel_fem_sv - vel_old) for offset in ti.grouped(ti.ndrange(stencil_size, stencil_size, stencil_size)): slot_idx = self.sph_solver.sh.grid_to_slot(base + offset) for k in range( self.sph_solver.sh.slot_start[slot_idx], self.sph_solver.sh.slot_start[slot_idx] + self.sph_solver.sh.slot_size[slot_idx], ): if ti.abs(pos - self.sph_solver.particles_reordered.pos[k]).max() < dx * 0.5: self.sph_solver.particles_reordered[k].vel = ( self.sph_solver.particles_reordered[k].vel - delta_mv / self.sph_solver.particles_info_reordered[k].mass ) self.fem_solver.elements_v[f + 1, iv].vel = vel_fem_sv # boundary condition for j in ti.static(range(3)): iv = self.fem_solver.surface[i].tri2v[j] _, self.fem_solver.elements_v[f + 1, iv].vel = self.fem_solver.boundary.impose_pos_vel( self.fem_solver.elements_v[f, iv].pos, self.fem_solver.elements_v[f + 1, iv].vel )
[ドキュメント] @ti.kernel def sph_rigid(self, f: ti.i32): for i in range(self.sph_solver._n_particles): if self.sph_solver.particles_ng_reordered[i].active: for i_g in range(self.rigid_solver.n_geoms): if self.rigid_solver.geoms_info[i_g].needs_coup: i_b = 0 self.sph_solver.particles_reordered[i].vel, self.sph_rigid_normal_reordered[i, i_g] = ( self._func_collide_with_rigid_geom_robust( self.sph_solver.particles_reordered[i].pos, self.sph_solver.particles_reordered[i].vel, self.sph_solver.particles_info_reordered[i].mass, self.sph_rigid_normal_reordered[i, i_g], i_g, i_b, ) )
[ドキュメント] @ti.kernel def pbd_rigid(self, f: ti.i32): for i in range(self.pbd_solver._n_particles): if self.pbd_solver.particles_ng_reordered[i].active: # NOTE: Couldn't figure out a good way to handle collision with non-free particle. Such collision is not phsically plausible anyway. for i_g in range(self.rigid_solver.n_geoms): if self.rigid_solver.geoms_info[i_g].needs_coup: i_b = 0 ( self.pbd_solver.particles_reordered[i].pos, self.pbd_solver.particles_reordered[i].vel, self.pbd_rigid_normal_reordered[i, i_g], ) = self._func_pbd_collide_with_rigid_geom( i, self.pbd_solver.particles_reordered[i].pos, self.pbd_solver.particles_reordered[i].vel, self.pbd_solver.particles_info_reordered[i].mass, self.pbd_rigid_normal_reordered[i, i_g], i_g, i_b, )
@ti.func def _func_pbd_collide_with_rigid_geom(self, i, pos_world, vel, mass, normal_prev, geom_idx, batch_idx): """ Resolves collision when a particle is already in collision with a rigid object. This function assumes known normal_rigid and influence. """ g_info = self.rigid_solver.geoms_info[geom_idx] signed_dist = self.rigid_solver.sdf.sdf_world(pos_world, geom_idx, batch_idx) vel_rigid = self.rigid_solver._func_vel_at_point(pos_world, g_info.link_idx, batch_idx) normal_rigid = self.rigid_solver.sdf.sdf_normal_world(pos_world, geom_idx, batch_idx) new_pos = pos_world if signed_dist < self.pbd_solver.particle_size / 2: # skip non-penetration particles rvel = vel - vel_rigid rvel_normal_magnitude = rvel.dot(normal_rigid) # negative if inward rvel_tan = rvel - rvel_normal_magnitude * normal_rigid rvel_tan_norm = rvel_tan.norm(gs.EPS) #################### rigid -> particle #################### stiffness = 1.0 # value in [0, 1] friction = 0.15 energy_loss = 0.0 # value in [0, 1] new_pos = pos_world + stiffness * normal_rigid * (self.pbd_solver.particle_size / 2 - signed_dist) v_norm = (new_pos - self.pbd_solver.particles_reordered[i].ipos) / self.pbd_solver._substep_dt delta_normal_magnitude = (v_norm - vel).dot(normal_rigid) delta_v_norm = delta_normal_magnitude * normal_rigid vel = v_norm #################### particle -> rigid #################### delta_mv = mass * delta_v_norm force = (-delta_mv / self.rigid_solver._substep_dt) * (1 - energy_loss) self.rigid_solver._func_apply_external_force(pos_world, force, g_info.link_idx, batch_idx) return new_pos, vel, normal_rigid
[ドキュメント] def preprocess(self, f): # preprocess for MPM CPIC if self.mpm_solver.is_active() and self.mpm_solver.enable_CPIC: self.mpm_surface_to_particle(f)
[ドキュメント] def couple(self, f): # MPM <-> all others if self.mpm_solver.is_active(): self.mpm_grid_op(f, self.sim.cur_t) # SPH <-> Rigid if self._rigid_sph: self.sph_rigid(f) # PBD <-> Rigid if self._rigid_pbd: self.pbd_rigid(f) if self.fem_solver.is_active(): self.fem_surface_force(f)
[ドキュメント] def couple_grad(self, f): if self.mpm_solver.is_active(): self.mpm_grid_op.grad(f, self.sim.cur_t) if self.fem_solver.is_active(): self.fem_surface_force.grad(f)
@property def active_solvers(self): """All the active solvers managed by the scene's simulator.""" return self.sim.active_solvers