def map_velocity_to_particles( previous_particle_velocity: PointCloud, velocity_grid: Grid, occupation_mask: Grid, previous_velocity_grid: Grid = None) -> PointCloud: """ Maps result of velocity projection on grid back to particles. Provides option to choose between FLIP (particle velocities are updated by the change between projected and initial grid velocities) and PIC (particle velocities are replaced by the the projected velocities) method depending on the value of the `initial_v_field`. Args: previous_particle_velocity: PointCloud with particle positions as elements and their corresponding velocities as values velocity_grid: Divergence-free velocity grid occupation_mask: Binary Grid (same type as `velocity_grid`) indicating which cells hold particles previous_velocity_grid: Velocity field before projection and force update. If None, the PIC method gets applied, FLIP otherwise Returns: PointCloud with particle positions as elements and updated particle velocities as values. """ if previous_velocity_grid is not None: # --- FLIP --- v_change_field = velocity_grid - previous_velocity_grid v_change_field, _ = extrapolate_valid(v_change_field, occupation_mask, 1) v_change = v_change_field.sample_at( previous_particle_velocity.elements.center) return previous_particle_velocity.with_( values=previous_particle_velocity.values + v_change) else: # --- PIC --- v_div_free_field, _ = extrapolate_valid(velocity_grid, occupation_mask, 1) v_values = v_div_free_field.sample_at( previous_particle_velocity.elements.center) return previous_particle_velocity.with_(values=v_values)
def extrapolation_helper(elements, t_shift, v_field, mask): shift = math.ceil(math.max( math.abs(elements.center - points.center))) - t_shift t_shift += shift v_field, mask = extrapolate_valid(v_field, mask, int(shift)) v_field *= accessible return v_field, mask, t_shift
def make_incompressible(velocity: StaggeredGrid, domain: Domain, particles: PointCloud, obstacles: tuple or list or StaggeredGrid = (), solve=math.Solve('auto', 1e-5, 0, gradient_solve=math.Solve('auto', 1e-5, 1e-5))): """ Projects the given velocity field by solving for the pressure and subtracting its spatial_gradient. Args: velocity: Current velocity field as StaggeredGrid domain: Domain object particles: `PointCloud` holding the current positions of the particles obstacles: Sequence of `phi.physics.Obstacle` objects or binary StaggeredGrid marking through-flow cell faces solve: Parameters for the pressure solve_linear Returns: velocity: divergence-free velocity of type `type(velocity)` pressure: solved pressure field, `CenteredGrid` iterations: Number of iterations required to solve_linear for the pressure divergence: divergence field of input velocity, `CenteredGrid` occupation_mask: StaggeredGrid """ points = particles.with_values(math.tensor(1., convert=True)) occupied_centered = points @ domain.scalar_grid() occupied_staggered = points @ domain.staggered_grid() if isinstance(obstacles, StaggeredGrid): accessible = obstacles else: accessible = domain.accessible_mask(union(*[obstacle.geometry for obstacle in obstacles]), type=StaggeredGrid) # --- Extrapolation is needed to exclude border divergence from the `occupied_centered` mask and thus # from the pressure solve_linear. If particles are randomly distributed, the `occupied_centered` mask # could sometimes include the divergence at the borders (due to single particles right at the edge # which temporarily deform the `occupied_centered` mask when moving into a new cell). This would then # get compensated by the pressure. This is unwanted for falling liquids and therefore prevented by this # extrapolation. --- velocity_field, _ = extrapolate_valid(velocity * occupied_staggered, occupied_staggered, 1) velocity_field *= accessible # Enforces boundary conditions after extrapolation div = field.divergence(velocity_field) * occupied_centered # Multiplication with `occupied_centered` excludes border divergence from pressure solve_linear @field.jit_compile_linear def matrix_eq(p): return field.where(occupied_centered, field.divergence(field.spatial_gradient(p, type=StaggeredGrid) * accessible), p) if solve.x0 is None: solve = copy_with(solve, x0=domain.scalar_grid()) pressure = field.solve_linear(matrix_eq, div, solve) def pressure_backward(_p, _p_, dp): return dp * occupied_centered.values, add_mask_in_gradient = math.custom_gradient(lambda p: p, pressure_backward) pressure = pressure.with_values(add_mask_in_gradient(pressure.values)) gradp = field.spatial_gradient(pressure, type=type(velocity_field)) * accessible return velocity_field - gradp, pressure, occupied_staggered
def map_velocity_to_particles(previous_particle_velocity: PointCloud, velocity_grid: Grid, occupation_mask: Grid, previous_velocity_grid: Grid = None, viscosity: float = 0.) -> PointCloud: """ Maps result of velocity projection on grid back to particles. Provides option to choose between FLIP (particle velocities are updated by the change between projected and initial grid velocities) and PIC (particle velocities are replaced by the the projected velocities) method depending on the value of the `initial_v_field`. Args: previous_particle_velocity: PointCloud with particle positions as elements and their corresponding velocities as values velocity_grid: Divergence-free velocity grid occupation_mask: Binary Grid (same type as `velocity_grid`) indicating which cells hold particles previous_velocity_grid: Velocity field before projection and force update viscosity: If previous_velocity_grid is None, the particle-in-cell method (PIC) is applied. Otherwise this is the ratio between FLIP and PIC (0. for pure FLIP) Returns: PointCloud with particle positions as elements and updated particle velocities as values. """ viscosity = min(max(0., viscosity), 1.) if previous_velocity_grid is None: viscosity = 1. velocities = math.zeros_like(previous_particle_velocity.values) if viscosity > 0.: # --- PIC --- velocity_grid, _ = extrapolate_valid(velocity_grid, occupation_mask) velocities += viscosity * (velocity_grid @ previous_particle_velocity).values if viscosity < 1.: # --- FLIP --- v_change_field = velocity_grid - previous_velocity_grid v_change_field, _ = extrapolate_valid(v_change_field, occupation_mask) v_change = (v_change_field @ previous_particle_velocity).values velocities += (1 - viscosity) * (previous_particle_velocity.values + v_change) return previous_particle_velocity.with_values(velocities)
def runge_kutta_4(cloud: SampledField, velocity: Field, dt: float, accessible: Field = None, occupied: Field = None): """ Lagrangian advection of particles using a fourth-order runge-kutta scheme. If `accessible` and `occupied` are specified, the advection uses velocity-dependent extrapolation of `velocity`. Args: cloud: PointCloud holding the particle positions as elements velocity: velocity Grid which should get used for advection dt: Time step for runge-kutta accessible: Boundary conditions for restricting extrapolation to accessible positions occupied: Binary Grid indicating particle positions on the grid for extrapolation Returns: PointCloud with advected particle positions and their corresponding values. """ warnings.warn( "runge_kutta_4 is deprecated. Use points() with integrator=rk4 instead.", DeprecationWarning) assert isinstance( velocity, Grid), 'runge_kutta advection with extrapolation works for Grids only.' def extrapolation_helper(elements, t_shift, v_field, mask): shift = math.ceil(math.max( math.abs(elements.center - points.center))) - t_shift t_shift += shift v_field, mask = extrapolate_valid(v_field, mask, int(shift)) v_field *= accessible return v_field, mask, t_shift points = cloud.elements total_shift = 0 extrapolate = accessible is not None and occupied is not None # --- Sample velocity at intermediate points and adjust velocity-dependent # extrapolation to maximum shift of corresponding component --- if extrapolate: assert isinstance( occupied, type(velocity)), 'occupation mask must have same type as velocity.' velocity, occupied = extrapolate_valid(velocity, occupied, 2) velocity *= accessible vel_k1 = sample(velocity, points) shifted_points = points.shifted(0.5 * dt * vel_k1) if extrapolate: velocity, occupied, total_shift = extrapolation_helper( shifted_points, total_shift, velocity, occupied) vel_k2 = sample(velocity, shifted_points) shifted_points = points.shifted(0.5 * dt * vel_k2) if extrapolate: velocity, occupied, total_shift = extrapolation_helper( shifted_points, total_shift, velocity, occupied) vel_k3 = sample(velocity, shifted_points) shifted_points = points.shifted(dt * vel_k3) if extrapolate: velocity, _, _ = extrapolation_helper(shifted_points, total_shift, velocity, occupied) vel_k4 = sample(velocity, shifted_points) # --- Combine points with RK4 scheme --- vel = (1 / 6.) * (vel_k1 + 2 * (vel_k2 + vel_k3) + vel_k4) new_points = points.shifted(dt * vel) return cloud.with_elements(new_points)
def make_incompressible( velocity: StaggeredGrid, domain: Domain, obstacles: tuple or list or StaggeredGrid = (), particles: PointCloud or None = None, solve_params: math.LinearSolve = math.LinearSolve(), pressure_guess: CenteredGrid = None ) -> Tuple[StaggeredGrid, CenteredGrid, math.Tensor, CenteredGrid, StaggeredGrid]: """ Projects the given velocity field by solving for the pressure and subtracting its spatial_gradient. Args: velocity: Current velocity field as StaggeredGrid obstacles: Sequence of `phi.physics.Obstacle` objects or binary StaggeredGrid marking through-flow cell faces particles (Optional if occupation masks are provided): Pointcloud holding the current positions of the particles domain (Optional if occupation masks are provided): Domain object pressure_guess (Optional): Initial pressure guess as CenteredGrid solve_params: Parameters for the pressure solve Returns: velocity: divergence-free velocity of type `type(velocity)` pressure: solved pressure field, `CenteredGrid` iterations: Number of iterations required to solve for the pressure divergence: divergence field of input velocity, `CenteredGrid` occupation_mask: StaggeredGrid """ points = particles.with_(values=math.wrap(1)) occupied_centered = points >> domain.grid() occupied_staggered = points >> domain.staggered_grid() if isinstance(obstacles, StaggeredGrid): accessible = obstacles else: accessible = domain.accessible_mask( union(*[obstacle.geometry for obstacle in obstacles]), type=StaggeredGrid) # --- Extrapolation is needed to exclude border divergence from the `occupied_centered` mask and thus # from the pressure solve. If particles are randomly distributed, the `occupied_centered` mask # could sometimes include the divergence at the borders (due to single particles right at the edge # which temporarily deform the `occupied_centered` mask when moving into a new cell) which would then # get compensated by the pressure. This is unwanted for falling liquids and therefore prevented by this # extrapolation. --- velocity_field, _ = extrapolate_valid(velocity * occupied_staggered, occupied_staggered, 1) velocity_field *= accessible # Enforces boundary conditions after extrapolation div = field.divergence( velocity_field ) * occupied_centered # Multiplication with `occupied_centered` excludes border divergence from pressure solve def matrix_eq(p): return field.where( occupied_centered, field.divergence( field.spatial_gradient(p, type=StaggeredGrid) * accessible), p) converged, pressure, iterations = field.solve(matrix_eq, div, pressure_guess or domain.grid(), solve_params=solve_params) gradp = field.spatial_gradient(pressure, type=type(velocity_field)) * accessible return velocity_field - gradp, pressure, iterations, div, occupied_staggered