Contact is a one-sided constraint
Let be signed separation: positive apart, zero touching, negative penetrating. Let be the normal impulse. A normal contact may push, never pull:
This is complementarity. Separated bodies need no impulse; an active contact may produce a nonnegative impulse. A practical time-stepping solver applies the same idea to predicted normal velocity and projects every trial impulse onto .
Experiment 1: derive the bounce impulse
An impulse changes linear momentum instantly. For two point masses, relative normal speed changes by:
Newton's restitution law asks for when bodies approach. Therefore:
The projection is essential. If , the bodies already separate, and a negative impulse would pull them back together.
Rigid bodies can also rotate. With offsets from centers of mass to the contact, the scalar effective inverse mass becomes:
Replace the denominator with . A push near a body's center is easier to translate; an off-center push must also create angular velocity.
Implement a non-pulling impulse
What should the solver do for approaching and separating velocities?

def normal_impulse(vn, inv_mass_a, inv_mass_b, restitution):
if vn >= 0: # already separating
return 0.0
k = inv_mass_a + inv_mass_b
return -(1 + restitution) * vn / kFailure modes: omit relative velocity, reverse the normal without swapping body order, forget rotational effective mass, or allow .
- Expected result
- The command exits successfully and reproduces the numerical or visual relationship described in this card.
- Failure modes
- Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
Experiment 2: predict a rebound
Drop from height without air resistance. Just before impact, . Restitution multiplies speed by , so energy—and therefore rebound height—scales with its square:
Sweep restitution
Does restitution 0.8 return 80% of height?

Failure modes: compare center height with surface clearance, include a later bounce in the first-apex measurement, expect , or use restitution to repair resting penetration.
- Expected result
- The command exits successfully and reproduces the numerical or visual relationship described in this card.
- Failure modes
- Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
Experiment 3: separate three jobs
Restitution targets impact velocity. Stabilization adds separating velocity when numerical integration has already produced penetration. A common bias is:
where is penetration. Compliance regularizes a rigid constraint into a soft one. At velocity level, LavenderSim uses :
These knobs interact. Larger steps permit deeper first overlap. Stronger stabilization may inject energy. Compliance improves conditioning and softness, but allows load-dependent deflection.
Sweep timestep and compliance
Which error belongs to discretization, and which belongs to the chosen soft-contact model?

Failure modes: treat compliance as an iteration count, tune bounce with penetration bias, divide compliance by instead of , or compare timesteps while changing simulated duration.
- Expected result
- The command exits successfully and reproduces the numerical or visual relationship described in this card.
- Failure modes
- Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
Experiment 4: inspect the contact rather than the pixels
At impact the one-kilogram sphere receives a short, large force. Once settled, its normal force approaches N. LavenderSim exposes aggregate body contact data and Chapter 8's individual contact trace:
observation, reward, terminated, truncated, info = sim.step()
contacts = observation["collision"]["contacts"]
normal_force = sim.body_contact_data()[ball_id, 1]
penetration = contacts[:, 8].max() if len(contacts) else 0.0Plot force beside penetration
Can telemetry distinguish an impact from resting support?

A force reported per discrete step is an average , not a resolved microsecond material-force curve. Shorter timesteps can produce taller-looking spikes while representing a similar impulse.
- Expected result
- The command exits successfully and reproduces the numerical or visual relationship described in this card.
- Failure modes
- Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
Experiment 5: remember yesterday's answer
A stack has coupled contacts: changing one impulse changes another contact's velocity. Projected Gauss–Seidel visits rows repeatedly. A cold start begins with zero impulses every step. A warm start matches persistent contacts and begins from the previous step's impulses, usually close to the new solution.
Compare cold and warm contact solves
With the same iteration budget, which initial guess leaves the smaller residual?

Production matching uses body pair, nearby contact point, and compatible normal. Bad matches can inject stale impulses, so caches need distance thresholds, normal checks, timestep scaling, and invalidation when topology changes.
- Expected result
- The command exits successfully and reproduces the numerical or visual relationship described in this card.
- Failure modes
- Non-finite values, a reversed trend, a failed assertion, or a materially different plot means the assumptions, seed, timestep, build, or backend should be inspected before continuing.
The experiment found—and fixed—a real solver bug
The first native sweep initially showed identical rebound for every restitution. The contact solver recomputed the restitution condition inside each of its 22 iterations. Iteration one created a bounce; iteration two observed separation, set the bounce target to zero, and removed most of that impulse.
LavenderSim now captures the pre-solve impact velocity once when the contact is created and holds that restitution target fixed throughout the solve. The native rebound regression test protects this behavior. This is precisely why a simulator tutorial should contain quantitative experiments rather than screenshots alone.
append_contact() captures the restitution bias, solve_contact_velocity() performs the projected compliant impulse update, and the persistent cache warm-starts matched contacts.Paw-check: make contact earn its force
- Derive the impulse for two movable masses instead of a ball and static floor.
- Move a contact away from a box's center and compute its rotational effective mass.
- Set and plot numerical energy over five bounces.
- Sweep stabilization gain and mark where rebound becomes artificially energetic.
- Change the stack's load slightly and quantify the warm-start advantage.
- Disable positional projection and measure steady penetration.
- Compare impulse across timesteps instead of peak force.
What should I be able to say now?
Normal contact is a unilateral constraint: it may push but not pull. Effective mass converts a desired change in relative point velocity into impulse. Restitution targets impact speed; stabilization repairs overlap; compliance softens and regularizes the constraint; projection enforces nonnegative impulses; and caching gives an iterative solver a better initial guess. Native force and penetration telemetry let us test each claim quantitatively.