Skip to content
Merged
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
109 changes: 109 additions & 0 deletions src/main/java/com/thealgorithms/physics/DampedOscillator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.thealgorithms.physics;

/**
* Models a damped harmonic oscillator, capturing the behavior of a mass-spring-damper system.
*
* <p>The system is defined by the second-order differential equation:
* x'' + 2 * gamma * x' + omega₀² * x = 0
* where:
* <ul>
* <li><b>omega₀</b> is the natural (undamped) angular frequency in radians per second.</li>
* <li><b>gamma</b> is the damping coefficient in inverse seconds.</li>
* </ul>
*
* <p>This implementation provides:
* <ul>
* <li>An analytical solution for the underdamped case (γ < ω₀).</li>
* <li>A numerical integrator based on the explicit Euler method for simulation purposes.</li>
* </ul>
*
* <p><strong>Usage Example:</strong>
* <pre>{@code
* DampedOscillator oscillator = new DampedOscillator(10.0, 0.5);
* double displacement = oscillator.displacementAnalytical(1.0, 0.0, 0.1);
* double[] nextState = oscillator.stepEuler(new double[]{1.0, 0.0}, 0.001);
* }</pre>
*
* @author [Yash Rajput](https://github.com/the-yash-rajput)
*/
public final class DampedOscillator {

/** Natural (undamped) angular frequency (rad/s). */
private final double omega0;

/** Damping coefficient (s⁻¹). */
private final double gamma;

private DampedOscillator() {
throw new AssertionError("No instances.");
}

/**
* Constructs a damped oscillator model.
*
* @param omega0 the natural frequency (rad/s), must be positive
* @param gamma the damping coefficient (s⁻¹), must be non-negative
* @throws IllegalArgumentException if parameters are invalid
*/
public DampedOscillator(double omega0, double gamma) {
if (omega0 <= 0) {
throw new IllegalArgumentException("Natural frequency must be positive.");
}
if (gamma < 0) {
throw new IllegalArgumentException("Damping coefficient must be non-negative.");
}
this.omega0 = omega0;
this.gamma = gamma;
}

/**
* Computes the analytical displacement of an underdamped oscillator.
* Formula: x(t) = A * exp(-γt) * cos(ω_d t + φ)
*
* @param amplitude the initial amplitude A
* @param phase the initial phase φ (radians)
* @param time the time t (seconds)
* @return the displacement x(t)
*/
public double displacementAnalytical(double amplitude, double phase, double time) {
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
return amplitude * Math.exp(-gamma * time) * Math.cos(omegaD * time + phase);
}

/**
* Performs a single integration step using the explicit Euler method.
* State vector format: [x, v], where v = dx/dt.
*
* @param state the current state [x, v]
* @param dt the time step (seconds)
* @return the next state [x_next, v_next]
* @throws IllegalArgumentException if the state array is invalid or dt is non-positive
*/
public double[] stepEuler(double[] state, double dt) {
if (state == null || state.length != 2) {
throw new IllegalArgumentException("State must be a non-null array of length 2.");
}
if (dt <= 0) {
throw new IllegalArgumentException("Time step must be positive.");
}

double x = state[0];
double v = state[1];
double acceleration = -2.0 * gamma * v - omega0 * omega0 * x;

double xNext = x + dt * v;
double vNext = v + dt * acceleration;

return new double[] {xNext, vNext};
}

/** @return the natural (undamped) angular frequency (rad/s). */
public double getOmega0() {
return omega0;
}

/** @return the damping coefficient (s⁻¹). */
public double getGamma() {
return gamma;
}
}
143 changes: 143 additions & 0 deletions src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.thealgorithms.physics;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

/**
* Unit tests for {@link DampedOscillator}.
*
* <p>Tests focus on:
* <ul>
* <li>Constructor validation</li>
* <li>Analytical displacement for underdamped and overdamped parameterizations</li>
* <li>Basic numeric integration sanity using explicit Euler for small step sizes</li>
* <li>Method argument validation (null/invalid inputs)</li>
* </ul>
*/
@DisplayName("DampedOscillator — unit tests")
public class DampedOscillatorTest {

private static final double TOLERANCE = 1e-3;

@Test
@DisplayName("Constructor rejects invalid parameters")
void constructorValidation() {
assertAll("invalid-constructor-params",
()
-> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(0.0, 0.1), "omega0 == 0 should throw"),
() -> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(-1.0, 0.1), "negative omega0 should throw"), () -> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(1.0, -0.1), "negative gamma should throw"));
}

@Test
@DisplayName("Analytical displacement matches expected formula for underdamped case")
void analyticalUnderdamped() {
double omega0 = 10.0;
double gamma = 0.5;
DampedOscillator d = new DampedOscillator(omega0, gamma);

double a = 1.0;
double phi = 0.2;
double t = 0.123;

// expected: a * exp(-gamma * t) * cos(omega_d * t + phi)
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
double expected = a * Math.exp(-gamma * t) * Math.cos(omegaD * t + phi);

double actual = d.displacementAnalytical(a, phi, t);
assertEquals(expected, actual, 1e-12, "Analytical underdamped displacement should match closed-form value");
}

@Test
@DisplayName("Analytical displacement gracefully handles overdamped parameters (omegaD -> 0)")
void analyticalOverdamped() {
double omega0 = 1.0;
double gamma = 2.0; // gamma > omega0 => omega_d = 0 in our implementation (Math.max)
DampedOscillator d = new DampedOscillator(omega0, gamma);

double a = 2.0;
double phi = Math.PI / 4.0;
double t = 0.5;

// With omegaD forced to 0 by implementation, expected simplifies to:
double expected = a * Math.exp(-gamma * t) * Math.cos(phi);
double actual = d.displacementAnalytical(a, phi, t);

assertEquals(expected, actual, 1e-12, "Overdamped handling should reduce to exponential * cos(phase)");
}

@Test
@DisplayName("Explicit Euler step approximates analytical solution for small dt over short time")
void eulerApproximatesAnalyticalSmallDt() {
double omega0 = 10.0;
double gamma = 0.5;
DampedOscillator d = new DampedOscillator(omega0, gamma);

double a = 1.0;
double phi = 0.0;

// initial conditions consistent with amplitude a and zero phase:
// x(0) = a, v(0) = -a * gamma * cos(phi) + a * omegaD * sin(phi)
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
double x0 = a * Math.cos(phi);
double v0 = -a * gamma * Math.cos(phi) - a * omegaD * Math.sin(phi); // small general form

double dt = 1e-4;
int steps = 1000; // simulate to t = 0.1s
double tFinal = steps * dt;

double[] state = new double[] {x0, v0};
for (int i = 0; i < steps; i++) {
state = d.stepEuler(state, dt);
}

double analyticAtT = d.displacementAnalytical(a, phi, tFinal);
double numericAtT = state[0];

// Euler is low-order — allow a small tolerance but assert it remains close for small dt + short time.
assertEquals(analyticAtT, numericAtT, TOLERANCE, String.format("Numeric Euler should approximate analytical solution at t=%.6f (tolerance=%g)", tFinal, TOLERANCE));
}

@Test
@DisplayName("stepEuler validates inputs and throws on null/invalid dt/state")
void eulerInputValidation() {
DampedOscillator d = new DampedOscillator(5.0, 0.1);

assertAll("invalid-stepEuler-args",
()
-> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(null, 0.01), "null state should throw"),
()
-> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0}, 0.01), "state array with invalid length should throw"),
() -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0, 0.0}, 0.0), "non-positive dt should throw"), () -> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0, 0.0}, -1e-3), "negative dt should throw"));
}

@Test
@DisplayName("Getter methods return configured parameters")
void gettersReturnConfiguration() {
double omega0 = Math.PI;
double gamma = 0.01;
DampedOscillator d = new DampedOscillator(omega0, gamma);

assertAll("getters", () -> assertEquals(omega0, d.getOmega0(), 0.0, "getOmega0 should return configured omega0"), () -> assertEquals(gamma, d.getGamma(), 0.0, "getGamma should return configured gamma"));
}

@Test
@DisplayName("Analytical displacement at t=0 returns initial amplitude * cos(phase)")
void analyticalAtZeroTime() {
double omega0 = 5.0;
double gamma = 0.2;
DampedOscillator d = new DampedOscillator(omega0, gamma);

double a = 2.0;
double phi = Math.PI / 3.0;
double t = 0.0;

double expected = a * Math.cos(phi);
double actual = d.displacementAnalytical(a, phi, t);

assertEquals(expected, actual, 1e-12, "Displacement at t=0 should be a * cos(phase)");
}
}