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 test_plot_point_cloud_2d_large(self): spheres = PointCloud( Sphere(wrap([(2, 4), (9, 8), (7, 8)], instance('points'), channel('vector')), radius=1)) cells = PointCloud( geom.pack_dims( CenteredGrid(0, 0, x=3, y=3, bounds=Box[4:6, 2:4]).elements, 'x,y', instance('points'))) cloud = field.stack([spheres, cells], instance('stack')) self._test_plot(cloud)
def test_plot_point_cloud_2d(self): spheres = PointCloud( Sphere(wrap([(.2, .4), (.9, .8), (.7, .8)], instance('points'), channel('vector')), radius=.1)) cells = PointCloud( geom.pack_dims( CenteredGrid(0, 0, x=3, y=3, bounds=Box[.4:.6, .2:.4]).elements, 'x,y', instance('points'))) cloud = field.stack([spheres, cells], instance('stack')) self._test_plot(cloud)
def points(self, points: Tensor or Number or tuple or list, values: Tensor or Number = None, radius: Tensor or float or int or None = None, extrapolation: math.Extrapolation = math.extrapolation.ZERO, color: str or Tensor or tuple or list or None = None) -> PointCloud: """ Create a `phi.field.PointCloud` from the given `points`. The created field has no channel dimensions and all points carry the value `1`. Args: points: point locations in physical units values: (optional) values of the particles, defaults to 1. radius: (optional) size of the particles extrapolation: (optional) extrapolation to use, defaults to extrapolation.ZERO color: (optional) color used when plotting the points Returns: `phi.field.PointCloud` object """ extrapolation = extrapolation if isinstance(extrapolation, math.Extrapolation) else self.boundaries[extrapolation] if radius is None: radius = math.mean(self.bounds.size) * 0.005 # --- Parse points: tuple / list --- if isinstance(points, (tuple, list)): if len(points) == 0: # no points points = math.zeros(instance(points=0), channel(vector=1)) elif isinstance(points[0], Number): # single point points = math.tensor([points], instance('points'), channel('vector')) else: points = math.tensor(points, instance('points'), channel('vector')) elements = Sphere(points, radius) if values is None: values = math.tensor(1.) return PointCloud(elements, values, extrapolation, add_overlapping=False, bounds=self.bounds, color=color)
def respect_boundaries(particles: PointCloud, domain: Domain, not_accessible: list, offset: float = 0.5) -> PointCloud: """ Enforces boundary conditions by correcting possible errors of the advection step and shifting particles out of obstacles or back into the domain. Args: particles: PointCloud holding particle positions as elements domain: Domain for which any particles outside should get shifted inwards not_accessible: List of Obstacle or Geometry objects where any particles inside should get shifted outwards offset: Minimum distance between particles and domain boundary / obstacle surface after particles have been shifted. Returns: PointCloud where all particles are inside the domain / outside of obstacles. """ new_positions = particles.elements.center for obj in not_accessible: if isinstance(obj, Obstacle): obj = obj.geometry new_positions = obj.push(new_positions, shift_amount=offset) new_positions = (~domain.bounds).push(new_positions, shift_amount=offset) return particles.with_( elements=Sphere(new_positions, math.mean(particles.bounds.size) * 0.005))
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 test_plot_multiple(self): grid = CenteredGrid(Noise(batch(b=2)), 0, Box[0:1, 0:1], x=50, y=10) grid2 = CenteredGrid(grid, 0, Box[0:2, 0:1], x=20, y=50) points = wrap([(.2, .4), (.9, .8)], instance('points'), channel('vector')) cloud = PointCloud(Sphere(points, radius=0.1), bounds=Box(0, [1, 1])) titles = math.wrap([['b=0', 'b=0', 'points'], ['b=1', 'b=1', '']], spatial('rows,cols')) self._test_plot(grid, grid2, cloud, row_dims='b', title=titles)
def test_convert_point_cloud(self): loc = math.random_uniform(instance(points=4), channel(vector=2)) val = math.random_normal(instance(points=4), channel(vector=2)) points = PointCloud(Sphere(loc, radius=1), val) for backend in BACKENDS: converted = field.convert(points, backend) self.assertEqual(converted.values.default_backend, backend) self.assertEqual(converted.elements.center.default_backend, backend) self.assertEqual(converted.elements.radius.default_backend, backend)
def test_overlay(self): grid = CenteredGrid(Noise(), extrapolation.ZERO, x=64, y=8, bounds=Box(0, [1, 1])) points = wrap([(.2, .4), (.9, .8)], instance('points'), channel('vector')) cloud = PointCloud(Sphere(points, radius=.1)) self._test_plot(overlay(grid, grid * (0.1, 0.02), cloud), title='Overlay')
def points(field: PointCloud, velocity: Field, dt: float, integrator=euler): """ Advects the sample points of a point cloud using a simple Euler step. Each point moves by an amount equal to the local velocity times `dt`. Args: field: point cloud to be advected velocity: velocity sampled at the same points as the point cloud dt: Euler step time increment integrator: ODE integrator for solving the movement. Returns: Advected point cloud """ new_elements = integrator(field.elements, velocity, dt) return field.with_elements(new_elements)
def points(field: PointCloud, velocity: PointCloud, dt): """ Advects the sample points of a point cloud using a simple Euler step. Each point moves by an amount equal to the local velocity times `dt`. Args: field: point cloud to be advected velocity: velocity sampled at the same points as the point cloud dt: Euler step time increment field: PointCloud: velocity: PointCloud: Returns: advected point cloud """ assert field.elements == velocity.elements new_points = field.elements.shifted(dt * velocity.values) return field.with_(elements=new_points)
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 test_plot_point_cloud_3d_points(self): self._test_plot( PointCloud( math.random_normal(instance(points=5), channel(vector='x,y,z'))))
def test_plot_point_cloud_3d(self): points = math.random_uniform(instance(points=50), channel(vector=3)) cloud = PointCloud(Sphere(points, radius=.1), bounds=Box(x=2, y=1, z=1)) self._test_plot(cloud)
def test_plot_point_cloud_vector_field_2d_bounded(self): points = math.random_uniform(instance(points='a,b,c,d,e'), channel(vector='x,y')) velocity = PointCloud(Sphere(points, radius=.1), bounds=Box(x=1, y=1)) self._test_plot(velocity * (.05, 0))
def test_plot_point_cloud_2d_bounded(self): points = wrap([(.2, .4), (.9, .8)], instance('points'), channel('vector')) self._test_plot( PointCloud(Sphere(points, radius=0.1), bounds=Box(0, [1, 1])))