Granular.jl

Julia package for granular dynamics simulation
git clone git://src.adamsgaard.dk/Granular.jl
Log | Files | Refs | README | LICENSE

commit 3241cf58da25204c6fc0cf2d20da73b1619ef790
parent 5e33f9737e5908bfdacf969015e884d4fbe45d3e
Author: Anders Damsgaard <andersd@riseup.net>
Date:   Wed, 20 Dec 2017 16:49:35 -0500

Add preliminary implementation of Poisson-disk sampling for irregular packings

Diffstat:
Msrc/packing.jl | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mtest/packing.jl | 14++++++++++++++
2 files changed, 103 insertions(+), 31 deletions(-)

diff --git a/src/packing.jl b/src/packing.jl @@ -4,7 +4,7 @@ export regularPacking! """ regularPacking!(simulation, n, r_min, r_max[, padding_factor, - size_distribution, size_distribution_parameter]) + size_distribution, size_distribution_parameter, seed]) Create a grid-based regular packing with grain numbers along each axis specified by the `n` vector. @@ -74,10 +74,13 @@ function generateNeighboringPoint(x_i::Vector, r_i::Real, r_j::Real, return [x_i[1] + R * sin(T), x_i[2] + R * cos(T)] end -export poissonDiscSampling +export irregularPacking! """ -Generate disc packing in 2D using Poisson disc sampling with O(N) complexity, as -described by [Robert Bridson (2007)](http://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf). +Generate a dense disc packing in 2D using Poisson disc sampling with O(N) +complexity, as described by [Robert Bridson (2007) "Fast Poisson disk sampling +in arbitrary dimensions"](https://doi.org/10.1145/1278780.1278807). The +`simulation` can be empty or already contain grains. However, an +`simulation.ocean` or `simulation.atmosphere` grid is required. # Arguments * `simulation::Simulation`: simulation object where grains are inserted. @@ -87,26 +90,29 @@ described by [Robert Bridson (2007)](http://www.cs.ubc.ca/~rbridson/docs/bridson before giving up. * `max_padding_factor::Real=2.`: this factor scales the padding to use during ice floe generation in numbers of grain diameters. +* `seed::Integer`: seed value to the pseudo-random number generator. * `verbose::Bool=true`: show diagnostic information to stdout. """ -function poissonDiscSampling(simulation::Simulation; - radius_max::Real=.1, - radius_min::Real=.1, - sample_limit::Integer=30, - max_padding_factor::Real=2., - thickness::Real=1., - verbose::Bool=true) +function irregularPacking!(simulation::Simulation; + radius_max::Real=.1, + radius_min::Real=.1, + sample_limit::Integer=30, + max_padding_factor::Real=2., + thickness::Real=1., + seed::Integer=1, + verbose::Bool=true) + srand(seed) active_list = Int[] # list of points to originate search from - # Step 0: Use existing `grid` (ocean or atmosphere) for contact-search grid + # Step 0: Use existing `grid` (ocean or atmosphere) for contact search if typeof(simulation.ocean.input_file) != Bool grid = simulation.ocean elseif typeof(simulation.atmosphere.input_file) != Bool grid = simulation.atmosphere else - error("poissonDiscSampling requires an ocean or atmosphere grid") + error("irregularPacking requires an ocean or atmosphere grid") end # save grid boundaries sw, se, ne, nw = getGridCornerCoordinates(grid.xq, grid.yq) @@ -114,7 +120,8 @@ function poissonDiscSampling(simulation::Simulation; width_y = nw[2] - sw[2] # assume regular grid # Step 1: If grid is empty: select random initial sample and save its index - # to the background grid. Else: Make all grains active for search + # to the background grid. Otherwise mark all existing grains as active + np_init = length(simulation.grains) if isempty(simulation.grains) r = rand()*(radius_max - radius_min) + radius_min x0 = rand(2).*[width_x, width_y] + sw @@ -128,37 +135,88 @@ function poissonDiscSampling(simulation::Simulation; end # Step 2: While the active list is not empty, choose a random index `i` from - # it. Generate up to `k` points chosen uniformly from the spherical annulus - # between radius `2*(r_i+r_j)` and `max_padding_factor*2*(r_i+r_j)` around - # `x_i`. - i = 0; j = 0; x_i = zeros(2); x_j = zeros(2); r_i = 0.; r_j = 0. + # it. Generate up to `sample_limit` points chosen uniformly from the + # spherical annulus between radius `2*(r_i+r_j)` and + # `max_padding_factor*2*(r_i+r_j)` around `x_i`. + # For each point in turn, check if it is within distance r of existing + # samples (using the background grid to only test nearby samples). If a + # point is adequately far from existing samples, emit it as the next sample + # and add it to the active list. If after `sample_limit` attempts no such + # point is found, instead remove `i` from the active list. + i = 0; j = 0; + x_active = zeros(2); x_candidate = zeros(2); + r_active = 0.; r_candidate = 0. + distance_modifier = zeros(2) + sw = zeros(2); se = zeros(2); ne = zeros(2); nw = zeros(2) + nx, ny = size(grid.xh) + n = 0 + while !isempty(active_list) i = rand(1:length(active_list)) deleteat!(active_list, i) - x_i = simulation.grains[i].lin_pos - r_i = simulation.grains[i].contact_radius - r_j = rand()*(radius_max - radius_min) + radius_min + x_active = simulation.grains[i].lin_pos + r_active = simulation.grains[i].contact_radius + r_candidate = rand()*(radius_max - radius_min) + radius_min for j=1:sample_limit - x_j = generateNeighboringPoint(x_i, r_i, r_j, max_padding_factor) - if !(isPointInGrid(grid, x_j)) - continue + x_candidate = generateNeighboringPoint(x_active, r_active, + r_candidate, + max_padding_factor) + + if !(isPointInGrid(grid, x_candidate)) + continue # skip this candidate + end + + # Inter-grain position vector and grain overlap + ix, iy = findCellContainingPoint(grid, x_candidate, sw, se, ne, nw) + no_overlaps_found = true + + # Check for overlap with existing grains + for ix_=(ix - 1):(ix + 1) + for iy_=(iy - 1):(iy + 1) + + # correct indexes if necessary + ix_corrected, iy_corrected = periodicBoundaryCorrection!( + grid, ix_, iy_, + distance_modifier) + + # skip iteration if target still falls outside grid after + # periodicity correction + if ix_corrected < 1 || ix_corrected > nx || + iy_corrected < 1 || iy_corrected > ny + continue + end + + for idx in grid.grain_list[ix_corrected, iy_corrected] + if norm(simulation.grains[idx].lin_pos - x_candidate + + distance_modifier) - + (simulation.grains[idx].contact_radius + + r_candidate) < 0. + + no_overlaps_found = false + break # overlap: skip this candidate + end + end + end end - # if it doesn't collide, add it to the active list - if !checkCollisions(grid, x_j, dx, position_list, radius_list) - push!(position_list, x_j) - push!(radius_list, r_j) - push!(active_list, length(radius_list)) + # if the grain candidate doesn't overlap with any other grains, add + # it to the active list + if no_overlaps_found + addGrainCylindrical!(simulation, x_candidate, r_candidate, + thickness, verbose=false) + sortGrainsInGrid!(simulation, grid) + push!(active_list, length(simulation.grains)) end end println("Active points: $(length(active_list))") + n += 1 + plotGrains(simulation, filetype="$n.png", show_figure=false) end if verbose - info("Generated $(length(radius_list)) points") + info("Generated $(length(simulation.grains) - np_init) points") end - return position_list, radius_list end diff --git a/test/packing.jl b/test/packing.jl @@ -1,4 +1,6 @@ #!/usr/bin/env julia +using Compat.Test +import Granular verbose = true @@ -35,3 +37,15 @@ for grain in sim.grains @test grain.contact_radius >= 1. @test grain.contact_radius <= 10. end + + +info("Testing irregular (Poisson-disk) packing generation (monodisperse size)") +sim = Granular.createSimulation() +sim.ocean = Granular.createRegularOceanGrid([1, 1, 1], [1., 1., 1.]) +Granular.irregularPacking!(sim, + radius_max=.1, + radius_min=.1, + sample_limit=30, + max_padding_factor=2., + thickness=1., + verbose=true)