1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! DEM granular contact models for spherical particle simulations.
//!
//! This crate provides the core physics for Discrete Element Method (DEM) simulations
//! of granular materials. It implements contact force models, rotational dynamics,
//! and granular temperature output.
//!
//! # Contact models
//!
//! ## Normal contact
//! - **Hertz** (default) — nonlinear elastic contact: `F_n = 4/3 E* √(R* δ) · δ`
//! with viscoelastic damping proportional to `β √(S_n m_r)`
//! - **Hooke** — linear spring contact: `F_n = k_n δ` with linear damping `γ_n v_n`
//! - **MDR** — adhesive elastic-plastic normal contact following the
//! LAMMPS/Zunker-Kamrin particle-pair rigid-flat transform for
//! loading/yield/unloading.
//!
//! ## Tangential contact
//! - **Mindlin** — incremental spring-history model with Coulomb friction cap `μ |F_n|`.
//! Spring displacement is stored per-contact and rotated to stay in the tangent plane
//! each step. Damping: `γ_t = 2 β √(5/6) √(k_t m_r)`.
//! - **Mindlin rescale** — LAMMPS-style unloading variants (`mindlin_rescale`,
//! `mindlin_rescale/force`) that scale the tangential history by the contact-radius
//! ratio when the normal contact unloads.
//! - **linear_nohistory** — velocity-Coulomb tangential damping with no spring history.
//!
//! ## Rolling resistance
//! - **Constant torque** (default) — `τ_r = μ_r |F_n| R*` opposing relative rolling
//! - **SDS** (spring-dashpot-slider) — incremental rolling displacement with spring
//! stiffness, viscous damping, and Coulomb-style slider cap
//!
//! ## Twisting friction
//! - **Constant torque** (default) — `τ_tw = μ_tw |F_n| R*` opposing relative twisting
//! - **SDS** (spring-dashpot-slider) — incremental twist angle with spring, damping, cap
//!
//! ## Adhesion / cohesion
//! - **JKR** — Johnson-Kendall-Roberts adhesion with extended interaction range beyond
//! geometric contact; pull-off force `F = 3/2 π γ R*`
//! - **DMT** — Derjaguin-Muller-Toporov adhesion with constant attractive force
//! `F = 2π γ R*` during contact only
//! - **SJKR** — simplified cohesion proportional to contact area: `F = k_coh π δ R*`
//!
//! > ⚠️ **Hooke/Hertz adhesion asymmetry.** JKR and DMT adhesion (driven by
//! > `surface_energy`) are implemented **only on the Hertz contact path**. Under
//! > `contact_model = "hooke"` the `surface_energy` term is *silently ignored* —
//! > the linear-spring path applies SJKR cohesion (`cohesion_energy`) only. If
//! > you need JKR/DMT pull-off, use the default Hertz model.
//!
//! # TOML configuration
//!
//! Contact model parameters are set per-material in the `[[dem.materials]]` array:
//!
//! ```toml
//! [[dem.materials]]
//! name = "glass"
//! youngs_modulus = 8.7e9 # Pa — Young's modulus E
//! poisson_ratio = 0.3 # dimensionless — Poisson's ratio ν
//! restitution = 0.95 # dimensionless — coefficient of restitution (0–1)
//! friction = 0.4 # dimensionless — sliding friction coefficient μ
//! rolling_friction = 0.1 # dimensionless — rolling friction coefficient μ_r
//! cohesion_energy = 0.0 # J/m² — SJKR cohesion energy density (0 = disabled)
//! surface_energy = 0.0 # J/m² — JKR/DMT surface energy γ (0 = disabled)
//! ```
//!
//! Global model selection:
//!
//! ```toml
//! [dem]
//! contact_model = "hertz" # "hertz" (default), "hooke", or "mdr"
//! tangential_model = "history" # "history", "linear_nohistory", "mindlin_rescale", "mindlin_rescale/force"
//! adhesion_model = "jkr" # "jkr" (default) or "dmt" (only when surface_energy > 0)
//! rolling_model = "constant" # "constant" (default) or "sds"
//! twisting_model = "constant" # "constant" (default), "sds", or "marshall" (coeffs derived from tangential)
//! ```
//!
//! # Material-parameter reference
//!
//! Every parameter above is stored per-material in [`dirt_atom::MaterialTable`]
//! and mixed into per-pair tables by `MaterialTable::build_pair_tables()`. Which
//! `MaterialTable` fields each model branch reads:
//!
//! | Model branch | `MaterialTable` inputs (per-pair table) |
//! |---|---|
//! | Hertz normal | `e_eff_ij` (E*), `beta_ij` (from `restitution`) |
//! | Hooke normal | `kn_ij` (harmonic mean of per-material `kn`), `beta_ij` |
//! | MDR normal | `e_eff_ij`, `g_eff_ij`, `surface_energy_ij`, `mdr_yield_stress_ij`, `mdr_damping_ij` |
//! | Mindlin tangential | `g_eff_ij` (G*), `friction_ij` (μ), `beta_ij` |
//! | Hooke tangential | `kt_ij`, `friction_ij` |
//! | Rolling (constant / SDS) | `rolling_friction_ij`, `rolling_stiffness_ij`, `rolling_damping_ij` |
//! | Twisting (constant / SDS) | `twisting_friction_ij`, `twisting_stiffness_ij`, `twisting_damping_ij` |
//! | JKR / DMT adhesion (Hertz only) | `surface_energy_ij` (γ), `adhesion_model` |
//! | SJKR cohesion | `cohesion_energy_ij` |
//!
//! `restitution` is the **restitution input `e`**: `beta_ij` is derived as
//! `β = α(e)/√5` with the Tsuji (1992) polynomial, matching LAMMPS `damping tsuji`
//! (see [`dirt_atom::hertz_beta_for_cor`]).
//!
//! # Tangential / rolling / twisting history (canonical frame)
//!
//! The Mindlin tangential force and the SDS rolling/twisting variants are
//! **incremental, history-dependent** springs: a displacement is integrated
//! across timesteps, rotated to stay in the current tangent plane, and capped at
//! a Coulomb limit. That history lives in
//! [`tangential::ContactHistoryStore`], which stores **8 `f64` per contact** —
//! `[0..3]` tangential spring vector (or elastic force for `/force` variants),
//! `[3..6]` rolling spring vector, `[6]` twisting scalar, and `[7]` the previous
//! contact radius used by Mindlin rescale variants (rolling/twisting slots are
//! zero under the constant-torque models).
//!
//! Each entry is kept in **canonical form** (from the lower-tag particle's
//! perspective) so the spring is frame-consistent no matter which particle is
//! `i` vs `j` in the neighbor list; a `sign` factor of `±1` flips the canonical
//! spring into the local `(i, j)` frame each step. The Coulomb friction limit is
//! applied in **two stages**: the stored spring is first capped so that
//! `|k_t s| ≤ μ|F_n|` (truncating the history that survives to the next step),
//! then the assembled force `F_t = k_t s − γ_t v_t` is capped again at `μ|F_n|`.
//!
//! # Modules
//!
//! - [`contact`] — Fused Hertz-Mindlin + Hooke contact force (primary code path)
//! - [`tangential`] — Per-contact tangential spring-history store (`ContactHistoryStore`)
//! - [`rotational`] — Quaternion-based velocity Verlet for angular degrees of freedom
//! - [`granular_temp`] — Granular temperature (velocity fluctuation) output;
//! the plugin ([`GranularTempPlugin`]) is **opt-in** and is *not* part of
//! [`GranularDefaultPlugins`] — add it explicitly when you want
//! `data/GranularTemp.txt` written.
// Public API documentation-completeness gate: every public item in this crate
// must carry a doc comment. Enforced on both `cargo build` (rustc) and
// `cargo doc` (rustdoc; e.g. `RUSTDOCFLAGS="-D missing_docs"`). Document real
// API intent here — do not add empty doc comments just to satisfy the lint.
pub use GranularTempPlugin;
pub use RotationalDynamicsPlugin;
use *;
use DemAtomInsertPlugin;
use DemAtomPlugin;
use VelocityVerletPlugin;
pub use HertzMindlinContactPlugin;
/// Re-export from [`dirt_atom`] for convenience.
pub use SQRT_5_6;
/// Small epsilon to avoid division by zero when normalizing tangential,
/// rolling, or twisting spring displacements.
pub const TANGENTIAL_EPSILON: f64 = 1e-30;
/// Warn when `distance / (r1 + r2)` falls below this threshold.
///
/// A ratio near 0.0 means nearly full overlap, which indicates an unstable
/// simulation (timestep too large or bad initial packing). Contacts with
/// overlap exceeding this threshold trigger a warning but still compute
/// forces (capped at half the smaller radius) to prevent runaway penetration.
pub const LARGE_OVERLAP_WARN_THRESHOLD: f64 = 0.90;
/// Maximum overlap warnings per timestep before the simulation panics.
///
/// If more than this many pairs exceed [`LARGE_OVERLAP_WARN_THRESHOLD`],
/// the simulation aborts with an actionable error message suggesting the
/// user reduce the timestep or fix the initial configuration.
pub const MAX_OVERLAP_WARNINGS: usize = 500;
/// Default DEM granular physics plugin group.
///
/// Includes, in registration order:
/// - [`DemAtomPlugin`] — per-atom material properties (radius, density) and
/// `MaterialTable` for per-material Young's modulus, Poisson ratio, restitution,
/// friction with geometric-mean mixing
/// - [`DemAtomInsertPlugin`] — random particle insertion from `[[particles.insert]]` config
/// - [`RotationalDynamicsPlugin`] — quaternion Velocity Verlet for angular degrees of freedom
/// (I = 2/5 m r² for solid spheres)
///
/// Granular temperature output ([`GranularTempPlugin`], which writes
/// `data/GranularTemp.txt`) is **not** bundled — add it explicitly only in the
/// examples that consume that file, so ordinary runs don't emit it.
///
/// Does **not** include infrastructure plugins (input, comm, domain, neighbor,
/// run, print). Use [`CorePlugins`] to get all infrastructure.
///
/// # Usage
/// ```rust,ignore
/// use dirt::prelude::*;
///
/// let mut app = App::new();
/// app.add_plugins(CorePlugins).add_plugins(GranularDefaultPlugins);
/// app.start();
/// ```
;