use super::*;
#[derive(Clone, Copy, Debug)]
pub(super) struct DemParticle {
pub(super) pos: [f64; 3],
pub(super) vel: [f64; 3],
pub(super) radius: f64,
pub(super) cutoff_padding: f64,
pub(super) density: f64,
pub(super) mat_idx: u32,
pub(super) tag: u32,
}
impl DemParticle {
pub(super) fn mass(self) -> f64 {
self.density * 4.0 / 3.0 * PI * self.radius.powi(3)
}
pub(super) fn write_core(self, atom: &mut Atom, i: usize, mass: f64) {
atom.tag[i] = self.tag;
atom.origin_index[i] = 0;
atom.cutoff_radius[i] = (self.radius + self.cutoff_padding.max(0.0)) as Real;
atom.image[i] = [0, 0, 0];
atom.is_ghost[i] = false;
atom.pos[i] = [
self.pos[0] as Real,
self.pos[1] as Real,
self.pos[2] as Real,
];
atom.vel[i] = [
self.vel[0] as Real,
self.vel[1] as Real,
self.vel[2] as Real,
];
atom.force[i] = [0.0; 3];
atom.mass[i] = mass as Real;
atom.inv_mass[i] = (1.0 / mass) as Real;
atom.atom_type[i] = self.mat_idx;
}
pub(super) fn write_dem(self, dem: &mut DemAtom, i: usize, mass: f64) {
dem.radius[i] = self.radius;
dem.density[i] = self.density;
dem.inv_inertia[i] = 1.0 / (0.4 * mass * self.radius * self.radius);
dem.quaternion[i] = [1.0, 0.0, 0.0, 0.0];
dem.omega[i] = [0.0; 3];
dem.ang_mom[i] = [0.0; 3];
dem.torque[i] = [0.0; 3];
dem.body_id[i] = 0.0;
}
}
pub(super) fn insert_single_particle(
atom: &mut Atom,
registry: &AtomDataRegistry,
row: DemParticle,
) {
let global_natoms = atom
.natoms
.checked_add(1)
.expect("global particle count overflow during DEM insertion");
ParticleStore::new(atom, registry)
.push_default_local(global_natoms)
.expect("registered DEM rows must accept transactional insertion");
let i = atom.len() - 1;
let mass = row.mass();
row.write_core(atom, i, mass);
let mut dem_data = registry.expect_mut::<DemAtom>("insert_single_particle");
row.write_dem(&mut dem_data, i, mass);
}
pub(super) fn owns_position(domain: &Domain, pos: &[f64; 3]) -> bool {
(0..3).all(|d| pos[d] >= domain.sub_domain_low[d] && pos[d] < domain.sub_domain_high[d])
}
pub(super) fn resolve_material(material_table: &MaterialTable, name: &str) -> Result<u32, String> {
material_table.find_material(name).ok_or_else(|| {
format!(
"unknown material '{}' in [[particles.insert]]. Available: {:?}",
name, material_table.names
)
})
}
pub(super) fn resolve_file_material(
material_table: &MaterialTable,
name: &str,
) -> Result<u32, InsertFileError> {
resolve_material(material_table, name).map_err(|message| InsertFileError::ParseField {
path: "config".to_string(),
line: 0,
field: "material".to_string(),
value: name.to_string(),
source: message,
})
}
pub(super) fn resolve_type_map(
type_map: &HashMap<String, String>,
material_table: &MaterialTable,
) -> Result<HashMap<u32, u32>, InsertFileError> {
let mut index_map = HashMap::new();
for (key_str, mat_name) in type_map {
let file_type: u32 = key_str
.parse()
.map_err(|_| InsertFileError::InvalidTypeMapKey {
key: key_str.clone(),
})?;
let mat_idx = resolve_material(material_table, mat_name).map_err(|message| {
InsertFileError::ParseField {
path: "config".to_string(),
line: 0,
field: "type_map material".to_string(),
value: mat_name.clone(),
source: message,
}
})?;
index_map.insert(file_type, mat_idx);
}
Ok(index_map)
}
pub(super) fn lookup_material_for_type(
file_type: u32,
type_index_map: Option<&HashMap<u32, u32>>,
default_mat_idx: u32,
) -> u32 {
if let Some(map) = type_index_map {
if let Some(&idx) = map.get(&file_type) {
return idx;
}
}
default_mat_idx
}
pub fn dem_insert_atoms(
comm: Res<CommResource>,
domain: Res<Domain>,
mut atom: ResMut<Atom>,
registry: Res<AtomDataRegistry>,
material_table: Res<MaterialTable>,
stage_overrides: Res<StageOverrides>,
run_config: Res<RunConfig>,
scheduler_manager: Res<SchedulerManager>,
mut rate_state: ResMut<RateInsertState>,
) {
let index = scheduler_manager.index;
let has_stage_particles = index < run_config.num_stages()
&& run_config
.current_stage(index)
.overrides
.contains_key("particles");
let particles_config: ParticlesConfig = if has_stage_particles || index == 0 {
stage_overrides.section("particles")
} else {
ParticlesConfig::default()
};
if let Some(ref inserts) = particles_config.insert {
{
let local_max_tag = atom.get_max_tag() as f64;
let mut max_tag = (-comm.all_reduce_min_f64(-local_max_tag)) as u32;
for insert in inserts {
if insert.source == "file" {
insert_from_file(
insert,
&mut atom,
®istry,
&material_table,
&domain,
&mut max_tag,
)
.expect("file insertion was fully parsed during fallible plugin preflight");
} else if is_rate_insert_config(insert) {
let mat_name = insert.material.as_deref().expect(
"rate insertion material was validated during fallible plugin preflight",
);
let prepared = prepare_random_insert(
insert,
&material_table,
&domain,
"rate-based [[particles.insert]]",
)
.expect("rate insertion was validated before setup");
let (rate, _, _) =
validate_rate_insert_config(insert, "Rate-based [[particles.insert]]")
.expect(
"rate insertion was validated during fallible plugin preflight",
);
println!(
"DemAtomInsert: registering rate-based insertion for material '{}' (rate={}/every {})",
mat_name,
rate,
insert.rate_interval.unwrap_or(1),
);
rate_state.entries.push(RateInsertEntry {
config: insert.clone(),
prepared,
total_inserted: 0,
});
} else {
let mat_name = insert.material.as_deref().expect(
"random insertion material was validated during fallible plugin preflight",
);
let count = insert.count.expect(
"random insertion count was validated during fallible plugin preflight",
);
let prepared = prepare_random_insert(
insert,
&material_table,
&domain,
"[[particles.insert]]",
)
.expect("random insertion was validated during fallible plugin preflight");
if comm.rank() == 0 {
println!(
"DemAtomInsert: inserting {} particles of material '{}' (r={}, rho={}, E={}, nu={})",
count,
mat_name,
prepared.max_radius,
prepared.density,
material_table.youngs_mod[prepared.mat_idx as usize],
material_table.poisson_ratio[prepared.mat_idx as usize]
);
}
let mut candidates =
CandidateGenerator::new(&prepared, &domain, prepared.seed, count as usize);
let mut inserted = 0u32;
let mut attempts = 0u64;
let max_attempts = count as u64 * 1_000_000;
while inserted < count && attempts < max_attempts {
attempts += 1;
let Some(candidate) = candidates.next(&prepared) else {
continue;
};
let tag = max_tag;
max_tag += 1;
if owns_position(&domain, &candidate.pos) {
insert_single_particle(
&mut atom,
®istry,
candidate.particle(&prepared, tag),
);
}
inserted += 1;
}
if inserted < count && comm.rank() == 0 {
eprintln!(
"WARNING: Could only insert {}/{} particles after {} attempts. \
Increase domain size or reduce particle count.",
inserted, count, max_attempts
);
}
}
}
}
}
}
pub(super) fn insert_from_file(
insert: &InsertConfig,
atom: &mut Atom,
registry: &AtomDataRegistry,
material_table: &MaterialTable,
domain: &Domain,
max_tag: &mut u32,
) -> Result<(), InsertFileError> {
let file_path = insert
.file
.as_deref()
.ok_or(InsertFileError::MissingField {
source: "particle",
field: "file",
})?;
let format = insert
.format
.as_deref()
.ok_or(InsertFileError::MissingField {
source: "particle",
field: "format",
})?;
match format {
"csv" => read_csv_particles(
insert,
file_path,
atom,
registry,
material_table,
domain,
max_tag,
),
"lammps_dump" => read_lammps_dump_particles(
insert,
file_path,
atom,
registry,
material_table,
domain,
max_tag,
),
"lammps_data" => read_lammps_data_particles(
insert,
file_path,
atom,
registry,
material_table,
domain,
max_tag,
),
other => Err(InsertFileError::UnknownFormat {
format: other.to_string(),
}),
}
}