#![deny(missing_docs)]
use grass_app::prelude::*;
use grass_scheduler::prelude::*;
use serde::Deserialize;
use dirt_schedule::{ADD_FORCE, SET_FORCE};
use soil_core::{
Accum, Atom, CommResource, Config, GroupDef, GroupRegistry, Optional, ParticleSimScheduleSet,
ParticlesWith, Real, ScheduleSetupSet, Write,
};
use soil_print::Thermo;
fn default_zero() -> f64 {
0.0
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct AddForceDef {
pub group: String,
#[serde(default = "default_zero")]
pub fx: f64,
#[serde(default = "default_zero")]
pub fy: f64,
#[serde(default = "default_zero")]
pub fz: f64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct SetForceDef {
pub group: String,
#[serde(default = "default_zero")]
pub fx: f64,
#[serde(default = "default_zero")]
pub fy: f64,
#[serde(default = "default_zero")]
pub fz: f64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct MoveLinearDef {
pub group: String,
#[serde(default = "default_zero")]
pub vx: f64,
#[serde(default = "default_zero")]
pub vy: f64,
#[serde(default = "default_zero")]
pub vz: f64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct FreezeDef {
pub group: String,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct ViscousDef {
pub group: String,
pub gamma: f64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct CundallDef {
pub group: String,
#[serde(default = "default_zero")]
pub gamma_l: f64,
#[serde(default = "default_zero")]
pub gamma_a: f64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct NveLimitDef {
pub group: String,
pub max_displacement: f64,
}
pub struct FixesRegistry {
pub add_forces: Vec<AddForceDef>,
pub set_forces: Vec<SetForceDef>,
pub move_linears: Vec<MoveLinearDef>,
pub freezes: Vec<FreezeDef>,
pub viscous: Vec<ViscousDef>,
pub cundall: Vec<CundallDef>,
pub nve_limit: Vec<NveLimitDef>,
}
pub struct FixesPlugin;
impl Plugin for FixesPlugin {
fn default_config(&self) -> Option<&str> {
Some(
r#"# [[addforce]]
# group = "fluid"
# fx = 0.1
# fy = 0.0
# fz = 0.0
# [[setforce]]
# group = "wall"
# fx = 0.0
# fy = 0.0
# fz = 0.0
# [[move_linear]]
# group = "piston"
# vx = 0.0
# vy = 0.0
# vz = -0.001
# [[freeze]]
# group = "frozen"
# [[nve_limit]]
# group = "all"
# max_displacement = 0.0001 # max distance any atom can move per step
# [[cundall]]
# group = "all"
# gamma_l = 0.8 # non-viscous linear damping fraction (Cundall/LAMMPS), [0,1]
# gamma_a = 0.8 # non-viscous angular damping fraction, [0,1]"#,
)
}
fn build(&self, app: &mut App) {
validate_fixes_config(app)
.unwrap_or_else(|error| panic!("FixesPlugin failed to build: {error}"));
let config = app
.get_resource_ref::<Config>()
.expect("Config resource must exist before FixesPlugin");
let registry = FixesRegistry {
add_forces: config.parse_array::<AddForceDef>("addforce"),
set_forces: config.parse_array::<SetForceDef>("setforce"),
move_linears: config.parse_array::<MoveLinearDef>("move_linear"),
freezes: config.parse_array::<FreezeDef>("freeze"),
viscous: config.parse_array::<ViscousDef>("viscous"),
cundall: config.parse_array::<CundallDef>("cundall"),
nve_limit: config.parse_array::<NveLimitDef>("nve_limit"),
};
drop(config);
let has_any = !registry.add_forces.is_empty()
|| !registry.set_forces.is_empty()
|| !registry.move_linears.is_empty()
|| !registry.freezes.is_empty()
|| !registry.viscous.is_empty()
|| !registry.cundall.is_empty()
|| !registry.nve_limit.is_empty();
if !has_any {
app.add_resource(registry);
return;
}
let has_move = !registry.move_linears.is_empty();
let has_add = !registry.add_forces.is_empty();
let has_set = !registry.set_forces.is_empty();
let has_freeze = !registry.freezes.is_empty();
let has_viscous = !registry.viscous.is_empty();
let has_cundall = !registry.cundall.is_empty();
let has_nve_limit = !registry.nve_limit.is_empty();
app.add_resource(registry)
.add_setup_system(setup_fixes, ScheduleSetupSet::PostSetup);
if has_move {
app.add_update_system(
apply_move_linear_pre,
ParticleSimScheduleSet::PreInitialIntegration,
);
app.add_update_system(apply_move_linear_post, ParticleSimScheduleSet::PostForce);
}
if has_add {
app.add_update_system(
apply_add_force.label(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
}
if has_set {
app.add_update_system(
apply_set_force.label(SET_FORCE).after(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
}
if has_freeze {
app.add_update_system(apply_freeze, ParticleSimScheduleSet::PostForce);
}
if has_viscous {
app.add_update_system(apply_viscous, ParticleSimScheduleSet::PostForce);
}
if has_cundall {
app.add_update_system(apply_cundall, ParticleSimScheduleSet::PostForce);
}
if has_nve_limit {
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
}
}
fn try_build(&self, app: &mut App) -> Result<(), AppError> {
validate_fixes_config(app)?;
self.build(app);
Ok(())
}
}
fn validate_fixes_config(app: &mut App) -> Result<(), AppError> {
let config = app
.get_resource_ref::<Config>()
.ok_or_else(|| AppError::message("FixesPlugin requires Config"))?;
let add_forces = config
.try_parse_array::<AddForceDef>("addforce")
.map_err(|error| AppError::message(error.to_string()))?;
let set_forces = config
.try_parse_array::<SetForceDef>("setforce")
.map_err(|error| AppError::message(error.to_string()))?;
let move_linears = config
.try_parse_array::<MoveLinearDef>("move_linear")
.map_err(|error| AppError::message(error.to_string()))?;
let freezes = config
.try_parse_array::<FreezeDef>("freeze")
.map_err(|error| AppError::message(error.to_string()))?;
let viscous = config
.try_parse_array::<ViscousDef>("viscous")
.map_err(|error| AppError::message(error.to_string()))?;
let cundall = config
.try_parse_array::<CundallDef>("cundall")
.map_err(|error| AppError::message(error.to_string()))?;
let nve_limit = config
.try_parse_array::<NveLimitDef>("nve_limit")
.map_err(|error| AppError::message(error.to_string()))?;
let group_defs = config
.try_parse_array::<GroupDef>("group")
.map_err(|error| AppError::message(error.to_string()))?;
let mut available_groups = vec!["all".to_string()];
available_groups.extend(group_defs.iter().map(|def| def.name.clone()));
for (defs, context) in [
(
add_forces.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix addforce",
),
(
set_forces.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix setforce",
),
(
move_linears
.iter()
.map(|def| &def.group)
.collect::<Vec<_>>(),
"fix move_linear",
),
(
freezes.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix freeze",
),
(
viscous.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix viscous",
),
(
cundall.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix cundall",
),
(
nve_limit.iter().map(|def| &def.group).collect::<Vec<_>>(),
"fix nve_limit",
),
] {
for group in defs {
if !available_groups.iter().any(|name| name == group) {
return Err(AppError::message(format!(
"{context}: group '{group}' not found; available groups: {}",
available_groups.join(", ")
)));
}
}
}
Ok(())
}
fn setup_fixes(registry: Res<FixesRegistry>, comm: Res<CommResource>, groups: Res<GroupRegistry>) {
for f in ®istry.add_forces {
groups
.validate_name(&f.group, "fix addforce")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.set_forces {
groups
.validate_name(&f.group, "fix setforce")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.move_linears {
groups
.validate_name(&f.group, "fix move_linear")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.freezes {
groups
.validate_name(&f.group, "fix freeze")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.viscous {
groups
.validate_name(&f.group, "fix viscous")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.cundall {
groups
.validate_name(&f.group, "fix cundall")
.expect("FixesPlugin preflight validates group names");
}
for f in ®istry.nve_limit {
groups
.validate_name(&f.group, "fix nve_limit")
.expect("FixesPlugin preflight validates group names");
}
if comm.rank() != 0 {
return;
}
if let Some(warning) = addforce_setforce_overlap_warning(®istry, &groups) {
eprintln!("{warning}");
}
for f in ®istry.add_forces {
println!(
"Fix addforce: group='{}', fx={}, fy={}, fz={}",
f.group, f.fx, f.fy, f.fz
);
}
for f in ®istry.set_forces {
println!(
"Fix setforce: group='{}', fx={}, fy={}, fz={}",
f.group, f.fx, f.fy, f.fz
);
}
for f in ®istry.move_linears {
println!(
"Fix move_linear: group='{}', vx={}, vy={}, vz={}",
f.group, f.vx, f.vy, f.vz
);
}
for f in ®istry.freezes {
println!("Fix freeze: group='{}'", f.group);
}
for f in ®istry.viscous {
println!("Fix viscous: group='{}', gamma={}", f.group, f.gamma);
}
for f in ®istry.cundall {
println!(
"Fix cundall: group='{}', gamma_l={}, gamma_a={}",
f.group, f.gamma_l, f.gamma_a
);
}
for f in ®istry.nve_limit {
println!(
"Fix nve_limit: group='{}', max_displacement={}",
f.group, f.max_displacement
);
}
}
fn addforce_setforce_overlap_warning(
registry: &FixesRegistry,
groups: &GroupRegistry,
) -> Option<String> {
let mut overlaps = Vec::new();
for add in ®istry.add_forces {
let add_group = groups.expect(&add.group);
for set in ®istry.set_forces {
let set_group = groups.expect(&set.group);
let overlapping_atoms = add_group
.mask
.iter()
.zip(&set_group.mask)
.filter(|(in_add, in_set)| **in_add && **in_set)
.count();
if overlapping_atoms > 0 {
overlaps.push(format!(
"addforce group '{}' overlaps setforce group '{}' on {} local atom(s)",
add.group, set.group, overlapping_atoms
));
}
}
}
if overlaps.is_empty() {
return None;
}
Some(format!(
"WARNING: overlapping [[addforce]] and [[setforce]] groups detected: {}. \
Both fixes run in PostForce; DIRT orders addforce before setforce, so setforce overwrites \
the accumulated force on overlapping atoms. Use disjoint groups if you expected additive behavior.",
overlaps.join("; ")
))
}
fn apply_move_linear_pre(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
for def in ®istry.move_linears {
let group = groups.expect(&def.group);
for i in 0..nlocal {
if group.mask[i] {
atoms.vel[i][0] = def.vx as Real;
atoms.vel[i][1] = def.vy as Real;
atoms.vel[i][2] = def.vz as Real;
}
}
}
}
fn apply_add_force(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
for def in ®istry.add_forces {
let group = groups.expect(&def.group);
for i in 0..nlocal {
if group.mask[i] {
atoms.force[i][0] += def.fx as Accum;
atoms.force[i][1] += def.fy as Accum;
atoms.force[i][2] += def.fz as Accum;
}
}
}
}
fn apply_set_force(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
for def in ®istry.set_forces {
let group = groups.expect(&def.group);
for i in 0..nlocal {
if group.mask[i] {
atoms.force[i][0] = def.fx as Accum;
atoms.force[i][1] = def.fy as Accum;
atoms.force[i][2] = def.fz as Accum;
}
}
}
}
fn apply_freeze(
mut atoms: ResMut<Atom>,
particles: ParticlesWith<'_, Optional<Write<dirt_atom::DemAtom>>>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
particles.with(|mut dem_opt| {
for def in ®istry.freezes {
let group = groups.expect(&def.group);
for i in 0..nlocal {
if group.mask[i] {
atoms.vel[i] = [0.0; 3];
atoms.force[i] = [0.0; 3];
if let Some(ref mut dem) = dem_opt {
if i < dem.omega.len() {
dem.omega[i] = [0.0; 3];
}
if i < dem.torque.len() {
dem.torque[i] = [0.0; 3];
}
if i < dem.ang_mom.len() {
dem.ang_mom[i] = [0.0; 3];
}
}
}
}
}
});
}
fn apply_move_linear_post(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
for def in ®istry.move_linears {
let group = groups.expect(&def.group);
for i in 0..nlocal {
if group.mask[i] {
atoms.force[i][0] = 0.0;
atoms.force[i][1] = 0.0;
atoms.force[i][2] = 0.0;
}
}
}
}
fn apply_viscous(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
for def in ®istry.viscous {
let group = groups.expect(&def.group);
let gamma = def.gamma;
for i in 0..nlocal {
if group.mask[i] {
atoms.force[i][0] -= (gamma * atoms.vel[i][0] as f64) as Accum;
atoms.force[i][1] -= (gamma * atoms.vel[i][1] as f64) as Accum;
atoms.force[i][2] -= (gamma * atoms.vel[i][2] as f64) as Accum;
}
}
}
}
fn apply_cundall(
mut atoms: ResMut<Atom>,
particles: ParticlesWith<'_, Optional<Write<dirt_atom::DemAtom>>>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
) {
let nlocal = atoms.nlocal as usize;
particles.with(|mut dem_opt| {
for def in ®istry.cundall {
let group = groups.expect(&def.group);
let gamma_l = def.gamma_l;
let gamma_a = def.gamma_a;
for i in 0..nlocal {
if !group.mask[i] {
continue;
}
for k in 0..3 {
let f = atoms.force[i][k] as f64;
let v = atoms.vel[i][k] as f64;
let signf = if f * v >= 0.0 { 1.0 } else { -1.0 };
atoms.force[i][k] = (f * (1.0 - gamma_l * signf)) as Accum;
}
if let Some(ref mut dem) = dem_opt {
if i < dem.torque.len() && i < dem.omega.len() {
for k in 0..3 {
let t = dem.torque[i][k];
let w = dem.omega[i][k];
let signt = if t * w >= 0.0 { 1.0 } else { -1.0 };
dem.torque[i][k] = t * (1.0 - gamma_a * signt);
}
}
}
}
}
});
}
fn apply_nve_limit(
mut atoms: ResMut<Atom>,
registry: Res<FixesRegistry>,
groups: Res<GroupRegistry>,
mut thermo: Option<ResMut<Thermo>>,
) {
let nlocal = atoms.nlocal as usize;
let dt = atoms.dt;
let mut n_limited: usize = 0;
for def in ®istry.nve_limit {
let group = groups.expect(&def.group);
let vmax = def.max_displacement / dt;
for i in 0..nlocal {
if !group.mask[i] {
continue;
}
let vx = atoms.vel[i][0] as f64;
let vy = atoms.vel[i][1] as f64;
let vz = atoms.vel[i][2] as f64;
let vmag = (vx * vx + vy * vy + vz * vz).sqrt();
if vmag > vmax {
let scale = vmax / vmag;
atoms.vel[i][0] = (atoms.vel[i][0] as f64 * scale) as Real;
atoms.vel[i][1] = (atoms.vel[i][1] as f64 * scale) as Real;
atoms.vel[i][2] = (atoms.vel[i][2] as f64 * scale) as Real;
n_limited += 1;
}
}
}
if let Some(ref mut t) = thermo {
t.set("n_limited", n_limited as f64);
}
}
#[derive(Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct GravityConfig {
#[serde(default)]
pub gx: f64,
#[serde(default)]
pub gy: f64,
#[serde(default = "default_gravity_gz")]
pub gz: f64,
}
impl Default for GravityConfig {
fn default() -> Self {
GravityConfig {
gx: 0.0,
gy: 0.0,
gz: -9.81,
}
}
}
fn default_gravity_gz() -> f64 {
-9.81
}
pub struct GravityPlugin;
impl Plugin for GravityPlugin {
fn default_config(&self) -> Option<&str> {
Some(
r#"[gravity]
# Gravitational acceleration components (m/s^2)
gx = 0.0
gy = 0.0
gz = -9.81"#,
)
}
fn build(&self, app: &mut App) {
Config::try_load::<GravityConfig>(app, "gravity")
.unwrap_or_else(|error| panic!("GravityPlugin failed to build: {error}"));
app.add_update_system(apply_gravity, ParticleSimScheduleSet::Force);
}
fn try_build(&self, app: &mut App) -> Result<(), AppError> {
Config::try_load::<GravityConfig>(app, "gravity")
.map_err(|error| AppError::message(error.to_string()))?;
app.add_update_system(apply_gravity, ParticleSimScheduleSet::Force);
Ok(())
}
}
pub fn apply_gravity(mut atoms: ResMut<Atom>, gravity: Res<GravityConfig>) {
for i in 0..atoms.nlocal as usize {
atoms.force[i][0] += (atoms.mass[i] as f64 * gravity.gx) as Accum;
atoms.force[i][1] += (atoms.mass[i] as f64 * gravity.gy) as Accum;
atoms.force[i][2] += (atoms.mass[i] as f64 * gravity.gz) as Accum;
}
}
#[cfg(test)]
mod tests {
use super::*;
use dirt_test_utils::{make_atoms, make_group_registry};
use soil_core::AtomDataRegistry;
#[test]
fn setforce_seam_does_not_require_an_addforce_provider() {
let mut app = App::new();
app.add_resource(make_atoms(1));
app.add_resource(make_group_registry("all", vec![true]));
app.add_resource(FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
});
app.add_update_system(
apply_set_force.label(SET_FORCE).after(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
app.organize_systems();
}
#[test]
fn typed_force_seams_keep_setforce_as_the_final_writer() {
let mut atoms = make_atoms(1);
atoms.force[0] = [10.0, 0.0, 0.0];
let registry = FixesRegistry {
add_forces: vec![AddForceDef {
group: "all".into(),
fx: 5.0,
fy: 0.0,
fz: 0.0,
}],
set_forces: vec![SetForceDef {
group: "all".into(),
fx: 1.0,
fy: 2.0,
fz: 3.0,
}],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(make_group_registry("all", vec![true]));
app.add_resource(registry);
app.add_update_system(
apply_add_force.label(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
app.add_update_system(
apply_set_force.label(SET_FORCE).after(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
app.organize_systems();
app.run();
assert_eq!(
app.get_resource_ref::<Atom>().unwrap().force[0],
[1.0, 2.0, 3.0]
);
}
#[test]
fn malformed_fix_is_a_typed_plugin_error() {
let mut app = App::new();
app.add_resource(Config::from_str(
r#"
[[addforce]]
group = "fluid"
misspelled_force = 1.0
"#,
));
let error = match app.try_add_plugins(FixesPlugin) {
Err(error) => error,
Ok(_) => panic!("malformed fix must fail plugin preflight"),
};
assert!(error.to_string().contains("addforce"));
}
#[test]
fn test_addforce_applies_constant_force() {
let mut atoms = make_atoms(3);
let groups = make_group_registry("fluid", vec![true, false, true]);
let registry = FixesRegistry {
add_forces: vec![AddForceDef {
group: "fluid".to_string(),
fx: 1.5,
fy: 0.0,
fz: -0.5,
}],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
atoms.force[0][0] = 2.0;
atoms.force[2][0] = 3.0;
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(apply_add_force, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.force[0][0] - 3.5).abs() < 1e-12); assert!((a.force[1][0]).abs() < 1e-12); assert!((a.force[2][0] - 4.5).abs() < 1e-12); assert!((a.force[0][2] - (-0.5)).abs() < 1e-12);
assert!((a.force[1][2]).abs() < 1e-12);
}
#[test]
fn test_setforce_overrides_force() {
let mut atoms = make_atoms(2);
atoms.force[0][0] = 100.0;
atoms.force[0][1] = 200.0;
atoms.force[0][2] = 300.0;
let groups = make_group_registry("wall", vec![true, false]);
let registry = FixesRegistry {
add_forces: vec![],
set_forces: vec![SetForceDef {
group: "wall".to_string(),
fx: 1.0,
fy: 2.0,
fz: 3.0,
}],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(apply_set_force, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.force[0][0] - 1.0).abs() < 1e-12);
assert!((a.force[0][1] - 2.0).abs() < 1e-12);
assert!((a.force[0][2] - 3.0).abs() < 1e-12);
}
#[test]
fn test_addforce_setforce_overlap_warns_with_final_semantics() {
let groups = make_group_registry("driven", vec![true, true, false]);
let mut groups = groups;
groups.groups.push(soil_core::Group {
name: "clamped".to_string(),
def: soil_core::GroupDef {
name: "clamped".to_string(),
atom_types: None,
region: None,
dynamic: Some(false),
},
mask: vec![false, true, true],
count: 2,
});
let registry = FixesRegistry {
add_forces: vec![AddForceDef {
group: "driven".to_string(),
fx: 1.0,
fy: 0.0,
fz: 0.0,
}],
set_forces: vec![SetForceDef {
group: "clamped".to_string(),
fx: 0.0,
fy: 0.0,
fz: 0.0,
}],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let warning = addforce_setforce_overlap_warning(®istry, &groups)
.expect("overlapping addforce/setforce groups must warn");
assert!(warning.contains("driven"));
assert!(warning.contains("clamped"));
assert!(warning.contains("1 local atom"));
assert!(warning.contains("addforce before setforce"));
assert!(warning.contains("setforce overwrites"));
assert!(warning.contains("expected additive behavior"));
}
#[test]
fn test_addforce_setforce_postforce_order_setforce_wins() {
let mut atoms = make_atoms(3);
atoms.force[0][0] = 10.0;
atoms.force[1][0] = 20.0;
atoms.force[2][0] = 30.0;
let groups = make_group_registry("all", vec![true, true, true]);
let registry = FixesRegistry {
add_forces: vec![AddForceDef {
group: "all".to_string(),
fx: 5.0,
fy: 0.0,
fz: 0.0,
}],
set_forces: vec![SetForceDef {
group: "all".to_string(),
fx: 1.0,
fy: 2.0,
fz: 3.0,
}],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_add_force.label(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
app.add_update_system(
apply_set_force.label(SET_FORCE).after(ADD_FORCE),
ParticleSimScheduleSet::PostForce,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
for force in &a.force[..3] {
assert_eq!(*force, [1.0, 2.0, 3.0]);
}
}
#[test]
fn test_freeze_zeros_vel_and_force() {
let mut atoms = make_atoms(3);
atoms.vel[1][0] = 5.0;
atoms.vel[1][1] = 6.0;
atoms.vel[1][2] = 7.0;
atoms.force[1][0] = 10.0;
atoms.force[1][1] = 20.0;
atoms.force[1][2] = 30.0;
let groups = make_group_registry("frozen", vec![false, true, false]);
let registry = FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![FreezeDef {
group: "frozen".to_string(),
}],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_resource(AtomDataRegistry::new());
app.add_update_system(apply_freeze, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.vel[1][0]).abs() < 1e-12);
assert!((a.vel[1][1]).abs() < 1e-12);
assert!((a.vel[1][2]).abs() < 1e-12);
assert!((a.force[1][0]).abs() < 1e-12);
assert!((a.force[1][1]).abs() < 1e-12);
assert!((a.force[1][2]).abs() < 1e-12);
}
#[test]
fn test_freeze_zeros_optional_dem_rotational_state_when_present() {
use dirt_test_utils::{ParticleFixture, ParticleSpec};
let mut fixture = ParticleFixture::single(ParticleSpec::new(0, [0.0; 3], 0.01)).build();
fixture.atom.vel[0] = [1.0, -2.0, 3.0];
fixture.atom.force[0] = [4.0, -5.0, 6.0];
let mut dem = fixture.registry.expect_mut::<dirt_atom::DemAtom>(
"test_freeze_zeros_optional_dem_rotational_state_when_present",
);
dem.omega[0] = [7.0, -8.0, 9.0];
dem.torque[0] = [10.0, -11.0, 12.0];
dem.ang_mom[0] = [13.0, -14.0, 15.0];
drop(dem);
let mut app = fixture.into_app();
app.add_resource(make_group_registry("frozen", vec![true]));
app.add_resource(FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![FreezeDef {
group: "frozen".to_string(),
}],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
});
app.add_update_system(apply_freeze, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let atom = app.get_resource_ref::<Atom>().unwrap();
assert_eq!(atom.vel[0], [0.0; 3]);
assert_eq!(atom.force[0], [0.0; 3]);
let registry = app.get_resource_ref::<AtomDataRegistry>().unwrap();
let dem = registry.get::<dirt_atom::DemAtom>().unwrap();
assert_eq!(dem.omega[0], [0.0; 3]);
assert_eq!(dem.torque[0], [0.0; 3]);
assert_eq!(dem.ang_mom[0], [0.0; 3]);
}
#[test]
fn test_move_linear_constant_velocity() {
let atoms = make_atoms(2);
let groups = make_group_registry("piston", vec![true, false]);
let registry = FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![MoveLinearDef {
group: "piston".to_string(),
vx: 0.0,
vy: 0.0,
vz: -0.5,
}],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_move_linear_pre,
ParticleSimScheduleSet::PreInitialIntegration,
);
app.add_update_system(apply_move_linear_post, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.vel[0][2] - (-0.5)).abs() < 1e-12);
assert!((a.vel[1][2]).abs() < 1e-12); assert!((a.force[0][0]).abs() < 1e-12); assert!((a.force[0][2]).abs() < 1e-12);
}
#[test]
fn test_viscous_opposes_velocity() {
let mut atoms = make_atoms(2);
atoms.vel[0][0] = 1.0;
atoms.vel[0][1] = -2.0;
atoms.vel[0][2] = 0.5;
let groups = make_group_registry("all", vec![true, true]);
let registry = FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![ViscousDef {
group: "all".to_string(),
gamma: 0.1,
}],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(apply_viscous, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.force[0][0] - (-0.1)).abs() < 1e-12, "fx = -gamma*vx");
assert!((a.force[0][1] - 0.2).abs() < 1e-12, "fy = -gamma*vy");
assert!((a.force[0][2] - (-0.05)).abs() < 1e-12, "fz = -gamma*vz");
}
#[test]
fn test_viscous_zero_at_rest() {
let atoms = make_atoms(2); let groups = make_group_registry("all", vec![true, true]);
let registry = FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![ViscousDef {
group: "all".to_string(),
gamma: 0.1,
}],
cundall: vec![],
nve_limit: vec![],
};
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(apply_viscous, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.force[0][0]).abs() < 1e-15);
assert!((a.force[0][1]).abs() < 1e-15);
assert!((a.force[0][2]).abs() < 1e-15);
}
fn cundall_registry(group: &str, gamma_l: f64, gamma_a: f64) -> FixesRegistry {
FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![CundallDef {
group: group.to_string(),
gamma_l,
gamma_a,
}],
nve_limit: vec![],
}
}
#[test]
fn test_cundall_linear_signed_force() {
let mut atoms = make_atoms(2);
atoms.vel[0] = [2.0, -3.0, -1.0];
atoms.force[0] = [4.0, 5.0, -6.0];
let groups = make_group_registry("all", vec![true, false]);
let g = 0.25_f64;
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(soil_core::AtomDataRegistry::new()); app.add_resource(cundall_registry("all", g, 0.5));
app.add_update_system(apply_cundall, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!(
(a.force[0][0] - 4.0 * (1.0 - g)).abs() < 1e-12,
"x: along motion -> (1-g)"
);
assert!(
(a.force[0][1] - 5.0 * (1.0 + g)).abs() < 1e-12,
"y: opposing -> (1+g)"
);
assert!(
(a.force[0][2] - (-6.0) * (1.0 - g)).abs() < 1e-12,
"z: along motion -> (1-g)"
);
}
#[test]
fn test_cundall_dissipates_power() {
let mut atoms = make_atoms(1);
atoms.vel[0] = [1.5, -0.7, 2.0];
atoms.force[0] = [3.0, 4.0, -1.0];
let p_before: f64 = (0..3)
.map(|k| atoms.force[0][k] as f64 * atoms.vel[0][k] as f64)
.sum();
let groups = make_group_registry("all", vec![true]);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(soil_core::AtomDataRegistry::new());
app.add_resource(cundall_registry("all", 0.8, 0.0));
app.add_update_system(apply_cundall, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
let p_after: f64 = (0..3)
.map(|k| a.force[0][k] as f64 * a.vel[0][k] as f64)
.sum();
assert!(
p_after < p_before,
"power must decrease: {p_after} !< {p_before}"
);
}
#[test]
fn test_cundall_angular_signed_torque() {
use dirt_test_utils::{ParticleFixture, ParticleSpec};
let mut fixture = ParticleFixture::single(ParticleSpec::new(0, [0.0; 3], 0.01))
.with_timestep(1e-6)
.build();
let atoms = &mut fixture.atom;
let mut dem = fixture
.registry
.expect_mut::<dirt_atom::DemAtom>("test_cundall_angular_signed_torque");
dem.omega[0] = [1.0, -1.0, -2.0];
dem.torque[0] = [2.0, 3.0, -4.0];
drop(dem);
atoms.vel[0] = [1.0, 0.0, 0.0];
atoms.force[0] = [5.0, 0.0, 0.0];
let groups = make_group_registry("all", vec![true]);
let ga = 0.3_f64;
let gl = 0.1_f64;
let mut app = fixture.into_app();
app.add_resource(groups);
app.add_resource(cundall_registry("all", gl, ga));
app.add_update_system(apply_cundall, ParticleSimScheduleSet::PostForce);
app.organize_systems();
app.run();
let reg = app
.get_resource_ref::<soil_core::AtomDataRegistry>()
.unwrap();
let dem = reg.get::<dirt_atom::DemAtom>().unwrap();
assert!(
(dem.torque[0][0] - 2.0 * (1.0 - ga)).abs() < 1e-12,
"Tx along spin -> (1-ga)"
);
assert!(
(dem.torque[0][1] - 3.0 * (1.0 + ga)).abs() < 1e-12,
"Ty opposing -> (1+ga)"
);
assert!(
(dem.torque[0][2] - (-4.0) * (1.0 - ga)).abs() < 1e-12,
"Tz along spin -> (1-ga)"
);
let a = app.get_resource_ref::<Atom>().unwrap();
assert!(
(a.force[0][0] - 5.0 * (1.0 - gl)).abs() < 1e-12,
"linear damped by gamma_l"
);
}
fn make_gravity_atom(mass: f64) -> Atom {
let mut atom = Atom::new();
atom.dt = 1e-6;
unsafe { atom.push_test_atom(0, [0.0; 3], 0.001, mass) };
atom.nlocal = 1;
atom.natoms = 1;
atom
}
#[test]
fn gravity_applies_force_equal_to_mg() {
let mass = 0.5;
let gz = -9.81;
let mut app = App::new();
app.add_resource(make_gravity_atom(mass));
app.add_resource(GravityConfig {
gx: 0.0,
gy: 0.0,
gz,
});
app.add_update_system(apply_gravity, ParticleSimScheduleSet::Force);
app.organize_systems();
app.run();
let atom = app.get_resource_ref::<Atom>().unwrap();
assert!((atom.force[0][0]).abs() < 1e-15);
assert!((atom.force[0][1]).abs() < 1e-15);
assert!((atom.force[0][2] - mass * gz).abs() < 1e-15);
}
#[test]
fn gravity_skips_ghost_atoms() {
let mass = 1.0;
let gz = -9.81;
let mut atom = make_gravity_atom(mass);
unsafe { atom.push_test_atom(1, [0.0; 3], 0.001, mass) };
atom.is_ghost[1] = true;
let mut app = App::new();
app.add_resource(atom);
app.add_resource(GravityConfig {
gx: 0.0,
gy: 0.0,
gz,
});
app.add_update_system(apply_gravity, ParticleSimScheduleSet::Force);
app.organize_systems();
app.run();
let atom = app.get_resource_ref::<Atom>().unwrap();
assert!((atom.force[0][2] - mass * gz).abs() < 1e-15);
assert!((atom.force[1][2]).abs() < 1e-15);
}
fn make_nve_limit_registry(group: &str, max_displacement: f64) -> FixesRegistry {
FixesRegistry {
add_forces: vec![],
set_forces: vec![],
move_linears: vec![],
freezes: vec![],
viscous: vec![],
cundall: vec![],
nve_limit: vec![NveLimitDef {
group: group.to_string(),
max_displacement,
}],
}
}
#[test]
fn nve_limit_caps_high_velocity() {
let mut atoms = make_atoms(1);
atoms.dt = 0.001;
atoms.vel[0] = [100.0, 0.0, 0.0];
let max_d = 0.01; let groups = make_group_registry("all", vec![true]);
let registry = make_nve_limit_registry("all", max_d);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
let vmag = (a.vel[0][0].powi(2) + a.vel[0][1].powi(2) + a.vel[0][2].powi(2)).sqrt();
let displacement = vmag * a.dt;
assert!(
(displacement - max_d).abs() < 1e-12,
"displacement {} should equal max_displacement {}",
displacement,
max_d
);
}
#[test]
fn nve_limit_does_not_change_small_velocity() {
let mut atoms = make_atoms(1);
atoms.dt = 0.001;
atoms.vel[0] = [0.6, 0.8, 0.0];
let max_d = 0.01;
let groups = make_group_registry("all", vec![true]);
let registry = make_nve_limit_registry("all", max_d);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.vel[0][0] - 0.6).abs() < 1e-15);
assert!((a.vel[0][1] - 0.8).abs() < 1e-15);
assert!((a.vel[0][2]).abs() < 1e-15);
}
#[test]
fn nve_limit_preserves_direction() {
let mut atoms = make_atoms(1);
atoms.dt = 0.001;
atoms.vel[0] = [3.0, 4.0, 0.0];
let max_d = 0.001; let groups = make_group_registry("all", vec![true]);
let registry = make_nve_limit_registry("all", max_d);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
let vmag = (a.vel[0][0].powi(2) + a.vel[0][1].powi(2) + a.vel[0][2].powi(2)).sqrt();
assert!(
(vmag - 1.0).abs() < 1e-12,
"vmag should be 1.0, got {}",
vmag
);
assert!(
(a.vel[0][0] / vmag - 0.6).abs() < 1e-12,
"direction x preserved"
);
assert!(
(a.vel[0][1] / vmag - 0.8).abs() < 1e-12,
"direction y preserved"
);
}
#[test]
fn nve_limit_zero_velocity_no_panic() {
let mut atoms = make_atoms(1);
atoms.dt = 0.001;
atoms.vel[0] = [0.0, 0.0, 0.0];
let groups = make_group_registry("all", vec![true]);
let registry = make_nve_limit_registry("all", 0.01);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!((a.vel[0][0]).abs() < 1e-15);
assert!((a.vel[0][1]).abs() < 1e-15);
assert!((a.vel[0][2]).abs() < 1e-15);
}
#[test]
fn nve_limit_respects_group_filter() {
let mut atoms = make_atoms(2);
atoms.dt = 0.001;
atoms.vel[0] = [100.0, 0.0, 0.0]; atoms.vel[1] = [100.0, 0.0, 0.0];
let groups = make_group_registry("limited", vec![true, false]);
let registry = make_nve_limit_registry("limited", 0.01);
let mut app = App::new();
app.add_resource(atoms);
app.add_resource(groups);
app.add_resource(registry);
app.add_update_system(
apply_nve_limit,
ParticleSimScheduleSet::PostFinalIntegration,
);
app.organize_systems();
app.run();
let a = app.get_resource_ref::<Atom>().unwrap();
assert!(
(a.vel[0][0] - 10.0).abs() < 1e-12,
"atom 0 should be capped"
);
assert!(
(a.vel[1][0] - 100.0).abs() < 1e-12,
"atom 1 should be unchanged"
);
}
}