I think I'm figuring it out. The main thing I was overlooking was that geom_point does not map to geom_point radius using the same units. So the collision_force wasn't working the way I expected, since the particles were being plotted only a fraction of their intended size. All was more predictable when I switched to ggforce::geom_circle.
This works out nicely, but would probably slow too much for large numbers of particles.

library(particles); library(tidyverse)
library(tidygraph); library(ggraph)
library(magick); library(animation)
n <- 50
y_precision <- 0.1
set.seed(0)
data <- tibble(x1 = rep(1:3, n), y1 = runif(n*3) * x1, radius = runif(n*3, min=0.02, 0.05))
max_radius <- max(data$radius)
# Plotting function
graph_plot <- . %>% {
gr <- as_tibble(.)
p <-
ggplot(gr, aes(x0 = x, y0 = y, r = radius, color = as.factor(x1), fill = as.factor(x1))) +
ggforce::geom_circle(alpha=1/2) +
guides(color = FALSE, fill = FALSE) +
coord_equal() + theme_minimal()
plot(p)
}
# Run simulation
tbl_graph(data) %>%
# Pre-populate at the positions in the table, but send the small particles moving to the right
simulate(setup = predefined_genesis(x1, y1, x_vel = 0.1 * max_radius / radius)) %>%
# Keep everything fairly close to the original y1 value
impose(y_constraint, ymin = y1 - y_precision, ymax = y1 + y_precision) %>%
# Run with these a few steps
evolve(5, on_generation = graph_plot) %>%
# Reheat, maybe unnecessary
reheat(alpha = 1) %>%
# Add force towards original x1, strongest for larger particles
wield(x_force, x = x1, strength = 2 * radius) %>%
# Add collision force
wield(collision_force, radius = radius, n_iter = 2, strength = 1) %>%
# Run 100 steps and continue plotting each
evolve(100, on_generation = graph_plot)
For my next steps, I'd like to get the centering better, which might involve fiddling with the x_force strength (proportional to radius^2?), and outputting as a GIF. Any suggestions?
Now my next step is to output as a GIF. Any hints?