Skip to content

Commit 4527bcb

Browse files
Adding DampedOscillator code
1 parent 3519a91 commit 4527bcb

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.thealgorithms.physics;
2+
3+
/**
4+
* Models a damped harmonic oscillator, capturing the behavior of a mass-spring-damper system.
5+
*
6+
* <p>The system is defined by the second-order differential equation:
7+
* x'' + 2 * gamma * x' + omega₀² * x = 0
8+
* where:
9+
* <ul>
10+
* <li><b>omega₀</b> is the natural (undamped) angular frequency in radians per second.</li>
11+
* <li><b>gamma</b> is the damping coefficient in inverse seconds.</li>
12+
* </ul>
13+
*
14+
* <p>This implementation provides:
15+
* <ul>
16+
* <li>An analytical solution for the underdamped case (γ < ω₀).</li>
17+
* <li>A numerical integrator based on the explicit Euler method for simulation purposes.</li>
18+
* </ul>
19+
*
20+
* <p><strong>Usage Example:</strong>
21+
* <pre>{@code
22+
* DampedOscillator oscillator = new DampedOscillator(10.0, 0.5);
23+
* double displacement = oscillator.displacementAnalytical(1.0, 0.0, 0.1);
24+
* double[] nextState = oscillator.stepEuler(new double[]{1.0, 0.0}, 0.001);
25+
* }</pre>
26+
*
27+
* @author [Yash Rajput](https://github.com/the-yash-rajput)
28+
*/
29+
public final class DampedOscillator {
30+
31+
/** Natural (undamped) angular frequency (rad/s). */
32+
private final double omega0;
33+
34+
/** Damping coefficient (s⁻¹). */
35+
private final double gamma;
36+
37+
private DampedOscillator() {
38+
throw new AssertionError("No instances.");
39+
}
40+
41+
/**
42+
* Constructs a damped oscillator model.
43+
*
44+
* @param omega0 the natural frequency (rad/s), must be positive
45+
* @param gamma the damping coefficient (s⁻¹), must be non-negative
46+
* @throws IllegalArgumentException if parameters are invalid
47+
*/
48+
public DampedOscillator(double omega0, double gamma) {
49+
if (omega0 <= 0) {
50+
throw new IllegalArgumentException("Natural frequency must be positive.");
51+
}
52+
if (gamma < 0) {
53+
throw new IllegalArgumentException("Damping coefficient must be non-negative.");
54+
}
55+
this.omega0 = omega0;
56+
this.gamma = gamma;
57+
}
58+
59+
/**
60+
* Computes the analytical displacement of an underdamped oscillator.
61+
* Formula: x(t) = A * exp(-γt) * cos(ω_d t + φ)
62+
*
63+
* @param amplitude the initial amplitude A
64+
* @param phase the initial phase φ (radians)
65+
* @param time the time t (seconds)
66+
* @return the displacement x(t)
67+
*/
68+
public double displacementAnalytical(double amplitude, double phase, double time) {
69+
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
70+
return amplitude * Math.exp(-gamma * time) * Math.cos(omegaD * time + phase);
71+
}
72+
73+
/**
74+
* Performs a single integration step using the explicit Euler method.
75+
* State vector format: [x, v], where v = dx/dt.
76+
*
77+
* @param state the current state [x, v]
78+
* @param dt the time step (seconds)
79+
* @return the next state [x_next, v_next]
80+
* @throws IllegalArgumentException if the state array is invalid or dt is non-positive
81+
*/
82+
public double[] stepEuler(double[] state, double dt) {
83+
if (state == null || state.length != 2) {
84+
throw new IllegalArgumentException("State must be a non-null array of length 2.");
85+
}
86+
if (dt <= 0) {
87+
throw new IllegalArgumentException("Time step must be positive.");
88+
}
89+
90+
double x = state[0];
91+
double v = state[1];
92+
double acceleration = -2.0 * gamma * v - (omega0 * omega0) * x;
93+
94+
double xNext = x + dt * v;
95+
double vNext = v + dt * acceleration;
96+
97+
return new double[] {xNext, vNext};
98+
}
99+
100+
/** @return the natural (undamped) angular frequency (rad/s). */
101+
public double getOmega0() {
102+
return omega0;
103+
}
104+
105+
/** @return the damping coefficient (s⁻¹). */
106+
public double getGamma() {
107+
return gamma;
108+
}
109+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package com.thealgorithms.physics;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
/**
9+
* Unit tests for {@link DampedOscillator}.
10+
*
11+
* <p>Tests focus on:
12+
* <ul>
13+
* <li>Constructor validation</li>
14+
* <li>Analytical displacement for underdamped and overdamped parameterizations</li>
15+
* <li>Basic numeric integration sanity using explicit Euler for small step sizes</li>
16+
* <li>Method argument validation (null/invalid inputs)</li>
17+
* </ul>
18+
*/
19+
@DisplayName("DampedOscillator — unit tests")
20+
public class DampedOscillatorTest {
21+
22+
private static final double TOLERANCE = 1e-3;
23+
24+
@Test
25+
@DisplayName("Constructor rejects invalid parameters")
26+
void constructorValidation() {
27+
assertAll("invalid-constructor-params",
28+
()
29+
-> assertThrows(IllegalArgumentException.class, () -> new DampedOscillator(0.0, 0.1), "omega0 == 0 should throw"),
30+
() -> 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"));
31+
}
32+
33+
@Test
34+
@DisplayName("Analytical displacement matches expected formula for underdamped case")
35+
void analyticalUnderdamped() {
36+
double omega0 = 10.0;
37+
double gamma = 0.5;
38+
DampedOscillator d = new DampedOscillator(omega0, gamma);
39+
40+
double A = 1.0;
41+
double phi = 0.2;
42+
double t = 0.123;
43+
44+
// expected: A * exp(-gamma * t) * cos(omega_d * t + phi)
45+
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
46+
double expected = A * Math.exp(-gamma * t) * Math.cos(omegaD * t + phi);
47+
48+
double actual = d.displacementAnalytical(A, phi, t);
49+
assertEquals(expected, actual, 1e-12, "Analytical underdamped displacement should match closed-form value");
50+
}
51+
52+
@Test
53+
@DisplayName("Analytical displacement gracefully handles overdamped parameters (omegaD -> 0)")
54+
void analyticalOverdamped() {
55+
double omega0 = 1.0;
56+
double gamma = 2.0; // gamma > omega0 => omega_d = 0 in our implementation (Math.max)
57+
DampedOscillator d = new DampedOscillator(omega0, gamma);
58+
59+
double A = 2.0;
60+
double phi = Math.PI / 4.0;
61+
double t = 0.5;
62+
63+
// With omegaD forced to 0 by implementation, expected simplifies to:
64+
double expected = A * Math.exp(-gamma * t) * Math.cos(phi);
65+
double actual = d.displacementAnalytical(A, phi, t);
66+
67+
assertEquals(expected, actual, 1e-12, "Overdamped handling should reduce to exponential * cos(phase)");
68+
}
69+
70+
@Test
71+
@DisplayName("Explicit Euler step approximates analytical solution for small dt over short time")
72+
void eulerApproximatesAnalyticalSmallDt() {
73+
double omega0 = 10.0;
74+
double gamma = 0.5;
75+
DampedOscillator d = new DampedOscillator(omega0, gamma);
76+
77+
double A = 1.0;
78+
double phi = 0.0;
79+
80+
// initial conditions consistent with amplitude A and zero phase:
81+
// x(0) = A, v(0) = -A * gamma * cos(phi) + A * omegaD * sin(phi)
82+
double omegaD = Math.sqrt(Math.max(0.0, omega0 * omega0 - gamma * gamma));
83+
double x0 = A * Math.cos(phi);
84+
double v0 = -A * gamma * Math.cos(phi) - A * omegaD * Math.sin(phi); // small general form
85+
86+
double dt = 1e-4;
87+
int steps = 1000; // simulate to t = 0.1s
88+
double tFinal = steps * dt;
89+
90+
double[] state = new double[] {x0, v0};
91+
for (int i = 0; i < steps; i++) {
92+
state = d.stepEuler(state, dt);
93+
}
94+
95+
double analyticAtT = d.displacementAnalytical(A, phi, tFinal);
96+
double numericAtT = state[0];
97+
98+
// Euler is low-order — allow a small tolerance but assert it remains close for small dt + short time.
99+
assertEquals(analyticAtT, numericAtT, TOLERANCE, String.format("Numeric Euler should approximate analytical solution at t=%.6f (tolerance=%g)", tFinal, TOLERANCE));
100+
}
101+
102+
@Test
103+
@DisplayName("stepEuler validates inputs and throws on null/invalid dt/state")
104+
void eulerInputValidation() {
105+
DampedOscillator d = new DampedOscillator(5.0, 0.1);
106+
107+
assertAll("invalid-stepEuler-args",
108+
()
109+
-> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(null, 0.01), "null state should throw"),
110+
()
111+
-> assertThrows(IllegalArgumentException.class, () -> d.stepEuler(new double[] {1.0}, 0.01), "state array with invalid length should throw"),
112+
() -> 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"));
113+
}
114+
115+
@Test
116+
@DisplayName("Getter methods return configured parameters")
117+
void gettersReturnConfiguration() {
118+
double omega0 = 3.14;
119+
double gamma = 0.01;
120+
DampedOscillator d = new DampedOscillator(omega0, gamma);
121+
122+
assertAll("getters", () -> assertEquals(omega0, d.getOmega0(), 0.0, "getOmega0 should return configured omega0"), () -> assertEquals(gamma, d.getGamma(), 0.0, "getGamma should return configured gamma"));
123+
}
124+
}

0 commit comments

Comments
 (0)