Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ path = "examples/example_template/main.rs"
name = "cone_hopper"
path = "examples/cone_hopper/main.rs"

[[example]]
name = "bench_multisphere_segregation"
path = "examples/bench_multisphere_segregation/main.rs"

[profile.release]
opt-level = 3
lto = "fat"
Expand Down
6 changes: 3 additions & 3 deletions crates/dem_clump/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ A **clump** is a rigid body composed of multiple overlapping spheres. Each spher

## TOML Configuration

Define clump types in `[[dem.clumps]]`:
Define clump types in `[[clump.definitions]]`:

```toml
[[dem.clumps]]
[[clump.definitions]]
name = "dimer"
spheres = [
{ offset = [-0.0003, 0.0, 0.0], radius = 0.001 },
Expand Down Expand Up @@ -58,7 +58,7 @@ app.run();
```

The plugin automatically:
- Loads clump definitions from `dem.clumps` config
- Loads clump definitions from `clump.definitions` config
- Registers per-atom clump data
- Aggregates forces from sub-spheres to parents
- Updates sub-sphere positions/velocities after parent integration
33 changes: 20 additions & 13 deletions crates/dem_clump/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! # Configuration
//!
//! ```toml
//! [[dem.clumps]]
//! [[clump.definitions]]
//! name = "dimer"
//! spheres = [
//! { offset = [-0.0003, 0.0, 0.0], radius = 0.001 },
Expand Down Expand Up @@ -49,7 +49,7 @@ pub struct ClumpSphereConfig {
pub radius: f64,
}

/// A clump type definition from `[[dem.clumps]]`.
/// A clump type definition from `[[clump.definitions]]`.
#[derive(Deserialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct ClumpDef {
Expand All @@ -59,17 +59,24 @@ pub struct ClumpDef {
pub spheres: Vec<ClumpSphereConfig>,
}

/// Extended DEM config with optional clump definitions.
/// Uses `#[serde(flatten)]` with a HashMap to absorb unknown `[dem]` keys
/// (materials, contact_model, etc.) without failing deserialization.
/// Top-level `[clump]` configuration section with clump type definitions.
///
/// Loaded from the `[clump]` TOML section (separate from `[dem]`) so that
/// `DemConfig`'s `deny_unknown_fields` doesn't reject the `clumps` key.
///
/// ```toml
/// [[clump.definitions]]
/// name = "dimer"
/// spheres = [
/// { offset = [-0.0003, 0.0, 0.0], radius = 0.001 },
/// { offset = [0.0003, 0.0, 0.0], radius = 0.001 },
/// ]
/// ```
#[derive(Deserialize, Clone, Default)]
pub struct DemClumpConfig {
pub struct ClumpPluginConfig {
/// Clump type definitions.
#[serde(default)]
pub clumps: Option<Vec<ClumpDef>>,
/// Pass-through fields we don't care about.
#[serde(flatten)]
pub _rest: std::collections::HashMap<String, toml::Value>,
pub definitions: Option<Vec<ClumpDef>>,
}

// ── Per-atom clump data ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -238,9 +245,9 @@ impl Plugin for ClumpPlugin {
// Load clump definitions from config
let mut registry = ClumpRegistry::new();

// Try to load dem.clumps from config
let dem_clump_config = Config::load::<DemClumpConfig>(app, "dem");
if let Some(clumps) = dem_clump_config.clumps {
// Load clump definitions from [clump] section
let clump_config = Config::load::<ClumpPluginConfig>(app, "clump");
if let Some(clumps) = clump_config.definitions {
for def in clumps {
assert!(
!def.spheres.is_empty(),
Expand Down
77 changes: 77 additions & 0 deletions examples/bench_multisphere_segregation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Multisphere Segregation Benchmark

## Physics

This benchmark validates the **clump/multisphere** implementation by simulating
size-driven segregation of single spheres vs dimer clumps under vertical
vibration — the **Brazil nut effect** for particle shape.

A 50/50 mixture of:
- **75 single spheres** (type 0, radius 1.0 mm)
- **75 dimer clumps** (type 1, two overlapping spheres with 1.0 mm offset)

is placed in a box with:
- Periodic lateral boundaries (x, y)
- A vertically oscillating floor (sinusoidal, z-direction)
- A static ceiling

### Vibration Parameters

| Parameter | Value |
|-----------|-------|
| Amplitude A | 2.0 mm |
| Frequency f | 15 Hz |
| Angular frequency ω | 94.25 rad/s |
| Dimensionless acceleration Γ = Aω²/g | **3.6** |

### Expected Behavior

Under vibration at Γ ≈ 3.6, the dimers have a larger effective size than single
spheres. The **Brazil nut effect** (also called granular convection / geometric
segregation) causes larger effective particles to rise to the top of the bed.

The **segregation index** is defined as:

$$S = \frac{z_{\text{dimer}} - z_{\text{sphere}}}{z_{\text{dimer}} + z_{\text{sphere}}}$$

where $z$ denotes the mass-weighted center-of-mass height. **S > 0** indicates
dimers are preferentially at the top.

### Assumptions / Simplifications

- 3D simulation with periodic lateral boundaries (quasi-infinite horizontal extent)
- Monodisperse spheres and identical dimers (no size distribution)
- Softened Young's modulus (5×10⁷ Pa) for computational efficiency
- Hertz–Mindlin contact with rolling friction
- No cohesion or adhesion

## Running

```bash
# Build and run (release mode, ~2-3 min)
cargo run --release --example bench_multisphere_segregation --no-default-features \
-- examples/bench_multisphere_segregation/config.toml

# Validate results (PASS/FAIL)
python3 examples/bench_multisphere_segregation/validate.py

# Generate plots
python3 examples/bench_multisphere_segregation/plot.py
```

## Output

- `data/segregation.csv` — step, time, z_sphere, z_dimer, segregation_index
- `data/Thermo.txt` — standard thermo output
- `segregation_index.png` — segregation index vs time
- `com_trajectories.png` — COM height trajectories

## Validation Criteria

| Check | Criterion |
|-------|-----------|
| No NaN/Inf | All data must be finite |
| Physical z | Both COM heights must be positive |
| S > 0 at steady state | Mean S in final 20% of run must be positive |
| Significant segregation | Mean final S > 0.005 |
| Positive trend | S increases (or is already high) in second half |
104 changes: 104 additions & 0 deletions examples/bench_multisphere_segregation/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Multisphere Segregation Benchmark
# ==================================
# A mixture of single spheres and dimer clumps in a vertically shaken box.
# Under vibration at Gamma ≈ 3.6, dimers (larger effective size) segregate
# upward due to the Brazil nut effect.
#
# Run with:
# cargo run --release --example bench_multisphere_segregation --no-default-features \
# -- examples/bench_multisphere_segregation/config.toml

[comm]
processors_x = 1
processors_y = 1
processors_z = 1

[domain]
# Box: 20mm x 20mm x 60mm (tall to allow segregation)
x_low = 0.0
x_high = 0.02 # [m]
y_low = 0.0
y_high = 0.02 # [m]
z_low = 0.0
z_high = 0.06 # [m]
periodic_x = true # Periodic lateral boundaries
periodic_y = true
periodic_z = false

[neighbor]
skin_fraction = 1.1 # [dimensionless]
bin_size = 0.004 # [m] — slightly larger than 2x particle diameter
rebuild_on_pbc_wrap = true

[gravity]
gz = -9.81 # Gravitational acceleration [m/s²]

# --- Material properties: glass-like ---
[[dem.materials]]
name = "glass"
youngs_mod = 5.0e7 # [Pa] — softened for faster timestep
poisson_ratio = 0.3 # [dimensionless]
restitution = 0.5 # [dimensionless] — moderate dissipation
friction = 0.5 # Sliding friction [dimensionless]
rolling_friction = 0.1 # Rolling friction [dimensionless]

# --- Dimer clump definition ---
# Two overlapping spheres offset along x, each with radius 1.0 mm.
# Center-to-center distance 1.0 mm → aspect ratio ≈ 1.5
# Note: clump definitions live under [clump], not [dem], because DemConfig
# uses deny_unknown_fields.
[[clump.definitions]]
name = "dimer"
spheres = [
{ offset = [-0.0005, 0.0, 0.0], radius = 0.001 },
{ offset = [ 0.0005, 0.0, 0.0], radius = 0.001 },
]

# --- Particle insertion ---
# Type 0: single spheres (75 particles, r = 1.0 mm)
[[particles.insert]]
material = "glass"
count = 75
radius = 0.001 # [m]
density = 2500.0 # [kg/m³]
region = { type = "block", min = [0.0, 0.0, 0.005], max = [0.02, 0.02, 0.04] }

# Type 1: dimer clumps (75 dimers)
[[particles.insert]]
material = "glass"
clump = "dimer"
count = 75
density = 2500.0 # [kg/m³]
region = { type = "block", min = [0.0, 0.0, 0.005], max = [0.02, 0.02, 0.04] }

# --- Walls ---
# Floor wall — oscillates sinusoidally along z
# Oscillation: A = 0.002 m, f = 15 Hz → ω = 2π·15 ≈ 94.25 rad/s
# Γ = A·ω²/g = 0.002 × 94.25² / 9.81 ≈ 3.6
[[wall]]
type = "plane"
point_z = 0.0
normal_z = 1.0
material = "glass"
name = "floor"
oscillate = { amplitude = 0.002, frequency = 15.0 }

# Ceiling wall — static, high above particles
[[wall]]
type = "plane"
point_z = 0.06
normal_z = -1.0
material = "glass"

[output]
dir = "examples/bench_multisphere_segregation/data"

[vtp]
interval = 10000 # Write VTP files every 10000 steps for visualization

# Single run: particles settle under gravity while floor oscillates.
# The initial transient includes both settling and the onset of segregation.
# Total runtime at ~1e-6 s timestep: ~0.5 s of physical time.
[run]
steps = 500000
thermo = 2000
Loading
Loading