mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
Adding DampedOscillator code (#6801)
* Adding DampedOscillator code * Adding one more test case * Adding one more test case * Adding one more test case * Fixing build issues. * Fixing build issues.
This commit is contained in:
109
src/main/java/com/thealgorithms/physics/DampedOscillator.java
Normal file
109
src/main/java/com/thealgorithms/physics/DampedOscillator.java
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user