Dynamics

class Dynamics<A : AgentLike> @JvmOverloads constructor(val space: ContinuousProjection<A>, var mass: (A) -> Double = { Defaults.unitMass }, maxSpeed: Double = Defaults.maxSpeed, minSpeed: Double = Defaults.minSpeed)(source)

Continuous-time dynamics primitive for agents on a ContinuousProjection. Owns the per-agent velocity state and the list of Forces contributing to motion; the step method performs the Euler integration step (sum forces → integrate velocity → compute candidate position) without applying it. Applying the new state — including boundary handling — is the caller's responsibility, because boundary policy varies between models (toroidal wrap for boids, walled rejection for pedestrians).

Typical use:

val dynamics = Dynamics<Boid>(space).apply {
addForce(separation(radius = 3.0))
addForce(alignment(radius = 8.0))
addForce(cohesion(radius = 10.0))
maxSpeed = 4.0
minSpeed = 1.0
}
// initial velocity
dynamics.setVelocity(boid, Point2D(...))

// in the agent's process { } body:
while (true) {
val (vNew, pNew) = dynamics.step(boid, dt = 0.05)
dynamics.setVelocity(boid, vNew)
space.moveTo(boid, wrap(pNew))
delay(0.05)
}

For the common "no boundary handling needed" case use runDynamics which packages the entire loop into one suspending call.

Mass and speed clamps validate via the Defaults convention (setters throw IllegalArgumentException on invalid values). Forces and velocities aren't validated — Force factories produce bounded values by design.

Parameters

space

the projection that owns agent positions

mass

per-agent mass function. Default is unit mass for all.

Constructors

Link copied to clipboard
constructor(space: ContinuousProjection<A>, mass: (A) -> Double = { Defaults.unitMass }, maxSpeed: Double = Defaults.maxSpeed, minSpeed: Double = Defaults.minSpeed)

Types

Link copied to clipboard
object Defaults

Mutable global defaults for Dynamics.

Properties

Link copied to clipboard

Number of forces currently registered.

Link copied to clipboard
var mass: (A) -> Double
Link copied to clipboard

Hard upper bound on velocity magnitude after each integration step. Must be positive and >= minSpeed.

Link copied to clipboard

Lower bound — velocities below this magnitude are rescaled back up to minSpeed. A body at exactly zero velocity is given a deterministic heading (a pure function of its identity, so runs stay reproducible) and boosted to minSpeed too, so a positive minSpeed truly guarantees the agent never comes to rest (boids). Set to 0 to allow agents to stop. Must be non-negative and <= maxSpeed.

Link copied to clipboard

Functions

Link copied to clipboard
fun addForce(force: Force<A>)

Append force to the summed forces. Forces are evaluated in order.

Link copied to clipboard
fun setVelocity(agent: A, v: Point2D)

Set / update the velocity tracked for agent.

Link copied to clipboard
fun step(agent: A, dt: Double): Pair<Point2D, Point2D>

One Euler integration step for agent: sum the registered forces, integrate velocity by (sumForce / mass) * dt, clamp to [minSpeed, maxSpeed], compute the candidate position pos + vNew * dt.

Link copied to clipboard

Compute one Euler step for every agent in agents without applying any of them — a Jacobi (synchronous) update. Every step reads the current shared state, so the result is independent of the order of agents, unlike calling step and applying per agent in a loop (which lets earlier agents' moves bias later ones — a Gauss-Seidel update that depends on iteration order). Use this when reproducibility / order independence matters, e.g. flocking.

Link copied to clipboard
fun untrack(agent: A)

Stop tracking agent (called when an agent leaves the population).

Link copied to clipboard
fun velocityOf(agent: A): Point2D?

Get the current velocity tracked for agent, or null if not set.