/** * DEMSolver — Discrete Element Method for granular-particle physics. * * ## Contact Model: Cundall–Strack Linear Spring-Dashpot * * Normal contact force (overlap δ = R_i + R_j - |r_ij| > 0): * * F_n = k_n · δ - c_n · v_rel_n * * Normal damping coefficient from coefficient of restitution e: * * c_n = -2 · ln(e) · sqrt(m_eff · k_n / (π² + ln²(e))) * * where m_eff = m_i · m_j / (m_i + m_j) is the reduced mass. * * Tangential: incremental spring with Coulomb slip cap: * * ΔF_t = k_t · Δu_t (incremental tangential displacement) * |F_t| ≤ μ · |F_n| (Coulomb cut) * * k_t defaults to 2/7 · k_n (common DEM practice). * * ## Neighbor Search: Uniform Grid * * Cell size = 2 · r_max ensures each particle overlaps at most the 27 * directly adjacent cells, keeping contact detection O(N). Grid arrays * are pre-allocated in the constructor and reused every step. * * ## Integration: Semi-Implicit Euler * * v(t+dt) = v(t) + (F/m) · dt * x(t+dt) = x(t) + v(t+dt) · dt * * Stability guard: dt ≤ safety · 2 · √(m_min / k_n) * (Rayleigh/contact-time estimate for a linear spring-dashpot contact). * When dt exceeds this limit the solver automatically sub-steps. * * ## Boundaries: Axis-Aligned Bounding Box (AABB) * * Each of the six wall faces applies the same spring-dashpot contact * model when a particle overlaps the wall (coefficient of restitution = e, * frictionless — walls have no tangential spring state). * * Wall contact convention: * n̂ points from wall toward the particle (into the interior). * Approach velocity: vRel_n = v · n̂ (negative = approaching wall). * fn = kn · δ − cn · vRel_n (clamped ≥ 0). * * ## Configuration (vault rule G.GOLD.485 — no Earth-hardcoded constants) * * All physical constants are config fields with documented defaults. * gravity defaults to Earth standard [0, -9.81, 0] — this is an Earth * default; change for other environments. * * @see SimSolver — generic solver interface consumed by the registry * @see MolecularDynamicsSolver — sibling particle solver, style reference */ import type { SolverMode, FieldData } from './SimSolver'; /** Full poly-disperse DEM configuration. */ export interface DEMConfig { /** Number of particles. */ particleCount: number; /** * Per-particle radius array (length = particleCount). * If omitted, all radii default to `defaultRadius`. */ radii?: Float64Array | number[]; /** * Per-particle mass array (length = particleCount). * If omitted, all masses default to `defaultMass`. */ masses?: Float64Array | number[]; /** Default radius when `radii` is not supplied (default: 0.05 m). */ defaultRadius?: number; /** Default mass when `masses` is not supplied (default: 1.0 kg). */ defaultMass?: number; /** * Normal contact stiffness k_n (N/m). * Default: 1e5 N/m — suitable for moderately stiff grains. */ kn?: number; /** * Tangential contact stiffness k_t (N/m). * Default: 2/7 · k_n (common DEM calibration). */ kt?: number; /** * Coefficient of restitution e ∈ (0, 1]. * e=1 → perfectly elastic (c_n=0); e→0 → maximally damped. * Default: 0.5. */ restitution?: number; /** * Coulomb friction coefficient μ ≥ 0. * Default: 0.3. */ friction?: number; /** * Gravity vector [gx, gy, gz] in m/s². * Default: [0, -9.81, 0] — Earth standard, vertical down. * Change for other planetary environments or zero-g experiments. */ gravity?: [number, number, number]; /** * AABB box bounds [[xMin, xMax], [yMin, yMax], [zMin, zMax]] in metres. * Default: [[-1,1], [-1,1], [-1,1]]. */ boxBounds?: [[number, number], [number, number], [number, number]]; /** * Safety factor for automatic sub-stepping. * dt_contact = safety · 2 · √(m_min / k_n); sub-step when dt > dt_contact. * Default: 0.2 (conservative). */ dtSafety?: number; /** * Initial particle positions as a flat array [x0,y0,z0, x1,y1,z1, ...]. * If not supplied the constructor distributes particles on a simple grid. */ initialPositions?: Float64Array | number[]; /** * Initial particle velocities as a flat array [vx0,vy0,vz0, ...]. * Defaults to zero. */ initialVelocities?: Float64Array | number[]; } export interface DEMStats { stepCount: number; currentTime: number; kineticEnergy: number; contactCount: number; maxOverlap: number; } export declare class DEMSolver { readonly mode: SolverMode; readonly fieldNames: readonly string[]; private readonly N; readonly positions: Float64Array; readonly velocities: Float64Array; private readonly forces; private readonly radii; private readonly masses; private readonly invMasses; private readonly kn; private readonly kt; private readonly restitution; private readonly friction; private readonly gravity; private readonly box; private readonly rMax; private readonly mMin; private readonly dtContact; private readonly _gridNx; private readonly _gridNy; private readonly _gridNz; private readonly _gridCellSize; private readonly _cellCount; private readonly _cellStart; private readonly _cellFill; private readonly _sortedParticles; private readonly _cellIdx; private readonly _activeContactSet; private stepCount; private currentTime; private kineticEnergy; private contactCount; private maxOverlap; /** * Tangential spring state keyed by contact pair. * Key = min(i,j)*N + max(i,j) for particle-particle contacts. * Wall contacts don't accumulate tangential spring state (walls are friction-free). */ private readonly tangentialSprings; /** * Per-contact tangential velocities collected during _particleContacts(). * Updated in _updateTangentialSprings() before the velocity integration. */ private readonly _tangentialVelocities; constructor(config: DEMConfig); /** * Default position initialization: pack particles on a simple 3-D grid * inside the box, using a seeded LCG for a small jitter so particles * aren't perfectly aligned (which can cause degenerate contacts). */ private _defaultPositions; /** * Advance the simulation by dt seconds. * * If dt > dtContact (stability limit), the step is automatically * sub-divided into smaller equal substeps. */ step(dt: number): void; /** No-op for transient solvers. */ solve(): void; getField(name: string): FieldData | null; getStats(): DEMStats; dispose(): void; private _substep; private _computeForces; /** * Detect all overlapping particle pairs using a pre-allocated uniform grid. * Cell size = 2·r_max (capped at 128 cells/axis). */ private _particleContacts; /** Apply forces for a single overlapping particle pair (i, j). */ private _resolveParticlePair; /** * Integrate tangential spring displacements using relative tangential * velocities collected during _resolveParticlePair(). */ private _updateTangentialSprings; /** Remove springs for contacts that were not active this step. */ private _purgeStaleSprings; /** * Apply spring-dashpot wall contact forces for each AABB face. * * Wall contact convention for wall W with inward unit normal n̂: * - δ = r − (signed gap to wall) — positive when overlapping * - vRel_n = v · n̂ (negative when approaching the wall) * - fn = kn·δ − cn·vRel_n (clamped ≥ 0) * - Applied force on particle: fn · n̂ */ private _wallContacts; } //# sourceMappingURL=DEMSolver.d.ts.map