use grass_app::App;
use grass_scheduler::{IntoScheduledSystem, ScheduleSet};
use soil_core::group::{Group, GroupDef};
use soil_core::{
Atom, AtomData, AtomDataRegistry, CommResource, GroupRegistry, Neighbor, SingleProcessComm,
};
pub const DEFAULT_DEM_TIMESTEP: f64 = 1e-7;
pub const MAX_TEST_DEM_TIMESTEP: f64 = 1e-6;
#[derive(Clone, Debug)]
pub struct ParticleSpec {
pub tag: u32,
pub position: [f64; 3],
pub radius: f64,
}
impl ParticleSpec {
pub fn new(tag: u32, position: [f64; 3], radius: f64) -> Self {
assert!(
radius.is_finite() && radius > 0.0,
"fixture particle radius must be finite and positive"
);
Self {
tag,
position,
radius,
}
}
}
#[derive(Clone, Debug)]
pub struct ParticleFixture {
particles: Vec<ParticleSpec>,
pairs: Vec<(usize, usize)>,
timestep: f64,
}
impl Default for ParticleFixture {
fn default() -> Self {
Self::pair(
ParticleSpec::new(0, [0.0, 0.0, 0.0], 0.001),
ParticleSpec::new(1, [0.0019, 0.0, 0.0], 0.001),
)
}
}
impl ParticleFixture {
pub fn new() -> Self {
Self::default()
}
pub fn single(particle: ParticleSpec) -> Self {
Self {
particles: vec![particle],
pairs: Vec::new(),
timestep: DEFAULT_DEM_TIMESTEP,
}
}
pub fn pair(first: ParticleSpec, second: ParticleSpec) -> Self {
Self {
particles: vec![first, second],
pairs: vec![(0, 1)],
timestep: DEFAULT_DEM_TIMESTEP,
}
}
pub fn with_timestep(mut self, timestep: f64) -> Self {
assert!(
timestep.is_finite() && timestep > 0.0 && timestep <= MAX_TEST_DEM_TIMESTEP,
"fixture timestep must be finite, positive, and <= {MAX_TEST_DEM_TIMESTEP:e}"
);
self.timestep = timestep;
self
}
pub fn push_particle(&mut self, particle: ParticleSpec) -> usize {
self.particles.push(particle);
self.particles.len() - 1
}
pub fn add_pair(&mut self, pair: (usize, usize)) {
assert!(
pair.0 < self.particles.len() && pair.1 < self.particles.len(),
"fixture pair index is outside the particle list"
);
assert_ne!(
pair.0, pair.1,
"fixture neighbour pair cannot be self-referential"
);
self.pairs.push(pair);
}
pub fn build(self) -> SimulationFixture {
let mut atom = Atom::new();
let mut dem = dirt_atom::DemAtom::new();
for particle in &self.particles {
push_dem_test_atom(
&mut atom,
&mut dem,
particle.tag,
particle.position,
particle.radius,
);
}
assert!(
!self.particles.is_empty(),
"SimulationFixture requires at least one particle"
);
atom.nlocal = self.particles.len() as u32;
atom.natoms = self.particles.len() as u64;
atom.dt = self.timestep;
let mut neighbor = Neighbor::new();
let mut rows = vec![Vec::new(); self.particles.len()];
for (i, j) in self.pairs {
rows[i].push(j as u32);
}
neighbor.neighbor_offsets.push(0);
for row in rows {
neighbor.neighbor_indices.extend(row);
neighbor
.neighbor_offsets
.push(neighbor.neighbor_indices.len() as u32);
}
let mut registry = AtomDataRegistry::new();
registry
.try_register(dem, atom.len())
.expect("fixture DEM extension must match Atom length");
SimulationFixture {
atom,
neighbor,
registry,
materials: make_material_table(),
}
}
}
pub struct SimulationFixture {
pub atom: Atom,
pub neighbor: Neighbor,
pub registry: AtomDataRegistry,
pub materials: dirt_atom::MaterialTable,
}
impl SimulationFixture {
pub fn register_atom_data<T: AtomData + 'static>(&mut self, data: T) {
self.registry
.try_register(data, self.atom.len())
.expect("fixture atom extension must match Atom length");
}
pub fn into_parts(self) -> (Atom, Neighbor, AtomDataRegistry, dirt_atom::MaterialTable) {
(self.atom, self.neighbor, self.registry, self.materials)
}
pub fn into_app(self) -> App {
let mut app = App::new();
app.add_resource(self.atom);
app.add_resource(self.neighbor);
app.add_resource(self.registry);
app.add_resource(self.materials);
app
}
pub fn into_scheduled_app<M>(
self,
system: impl IntoScheduledSystem<M>,
set: impl ScheduleSet,
) -> App {
let mut app = self.into_app();
app.add_update_system(system, set);
app
}
}
pub fn make_atoms(n: usize) -> Atom {
let mut atom = Atom::new();
for i in 0..n {
unsafe { atom.push_test_atom(i as u32, [i as f64, 0.0, 0.0], 0.5, 1.0) };
}
atom.nlocal = n as u32;
atom.natoms = n as u64;
atom.dt = 0.001;
atom
}
pub fn make_group_registry(name: &str, mask: Vec<bool>) -> GroupRegistry {
let count = mask.iter().filter(|&&m| m).count();
let mut registry = GroupRegistry::new();
registry.groups.push(Group {
name: name.to_string(),
def: GroupDef {
name: name.to_string(),
atom_types: None,
region: None,
dynamic: None,
},
mask,
count,
});
registry
}
pub fn make_single_comm() -> CommResource {
CommResource(Box::new(SingleProcessComm::new()))
}
pub fn push_dem_test_atom(
atom: &mut Atom,
dem: &mut dirt_atom::DemAtom,
tag: u32,
pos: [f64; 3],
radius: f64,
) {
let density = 2500.0;
let mass = density * 4.0 / 3.0 * std::f64::consts::PI * radius.powi(3);
unsafe { atom.push_test_atom(tag, pos, radius, mass) };
dem.radius.push(radius);
dem.density.push(density);
dem.inv_inertia.push(1.0 / (0.4 * mass * radius * radius));
dem.quaternion.push([1.0, 0.0, 0.0, 0.0]);
dem.omega.push([0.0; 3]);
dem.ang_mom.push([0.0; 3]);
dem.torque.push([0.0; 3]);
dem.body_id.push(0.0);
}
pub fn make_material_table() -> dirt_atom::MaterialTable {
let mut mt = dirt_atom::MaterialTable::new();
mt.add(
dirt_atom::Material::new("glass", dirt_atom::Elastic::new(8.7e9, 0.3, 0.95)).with_friction(
dirt_atom::Friction {
sliding: 0.4,
..Default::default()
},
),
)
.expect("test glass material is valid");
mt.build_pair_tables();
mt
}
#[cfg(test)]
mod fixture_tests {
use super::*;
use soil_core::ParticleSimScheduleSet;
#[test]
fn default_fixture_is_nonempty_and_synchronized() {
let fixture = ParticleFixture::default().build();
assert_eq!(fixture.atom.nlocal, 2);
assert_eq!(fixture.atom.natoms, 2);
assert_eq!(fixture.atom.len(), 2);
assert_eq!(
fixture
.registry
.expect::<dirt_atom::DemAtom>("fixture test")
.len(),
2
);
assert_eq!(fixture.neighbor.neighbor_offsets, vec![0, 1, 1]);
assert_eq!(fixture.neighbor.neighbor_indices, vec![1]);
assert_eq!(fixture.atom.dt, DEFAULT_DEM_TIMESTEP);
assert_eq!(fixture.materials.e_eff_ij.len(), 1);
assert_eq!(fixture.materials.e_eff_ij[0].len(), 1);
}
#[test]
fn declared_pairs_become_csr_rows() {
let mut builder = ParticleFixture::single(ParticleSpec::new(4, [0.0; 3], 0.002));
let second = builder.push_particle(ParticleSpec::new(9, [1.0, 0.0, 0.0], 0.002));
builder.add_pair((0, second));
let fixture = builder.build();
assert_eq!(fixture.neighbor.neighbor_offsets, vec![0, 1, 1]);
assert_eq!(fixture.neighbor.neighbor_indices, vec![1]);
assert_eq!(fixture.atom.tag, vec![4, 9]);
}
#[test]
#[should_panic(expected = "fixture timestep")]
fn rejects_unstable_timestep() {
let _ = ParticleFixture::new().with_timestep(0.01);
}
fn mark_first_atom(mut atom: grass_scheduler::ResMut<Atom>) {
atom.force[0][0] = 42.0;
}
#[test]
fn can_schedule_a_system_without_manual_resource_wiring() {
let mut app = ParticleFixture::new()
.build()
.into_scheduled_app(mark_first_atom, ParticleSimScheduleSet::Force);
app.organize_systems();
app.run();
assert_eq!(app.get_resource_ref::<Atom>().unwrap().force[0][0], 42.0);
}
}