From 4527bcb8bd5ca06bea8a7c378854bb6004c7607b Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 20:52:11 +0530 Subject: [PATCH 1/6] Adding DampedOscillator code --- .../physics/DampedOscillator.java | 109 +++++++++++++++ .../physics/DampedOscillatorTest.java | 124 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 src/main/java/com/thealgorithms/physics/DampedOscillator.java create mode 100644 src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java diff --git a/src/main/java/com/thealgorithms/physics/DampedOscillator.java b/src/main/java/com/thealgorithms/physics/DampedOscillator.java new file mode 100644 index 000000000000..f98def45e2e1 --- /dev/null +++ b/src/main/java/com/thealgorithms/physics/DampedOscillator.java @@ -0,0 +1,109 @@ +package com.thealgorithms.physics; + +/** + * Models a damped harmonic oscillator, capturing the behavior of a mass-spring-damper system. + * + *

The system is defined by the second-order differential equation: + * x'' + 2 * gamma * x' + omega₀² * x = 0 + * where: + *

+ * + *

This implementation provides: + *

+ * + *

Usage Example: + *

{@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);
+ * }
+ * + * @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; + } +} diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java new file mode 100644 index 000000000000..6da757e1ada1 --- /dev/null +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -0,0 +1,124 @@ +package com.thealgorithms.physics; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link DampedOscillator}. + * + *

Tests focus on: + *

+ */ +@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 = 3.14; + 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")); + } +} From 93482390dfdc9793226580e1cbb1bb1491672ea3 Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 20:58:34 +0530 Subject: [PATCH 2/6] Adding one more test case --- .../physics/DampedOscillatorTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java index 6da757e1ada1..29e098e1a870 100644 --- a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -121,4 +121,21 @@ void gettersReturnConfiguration() { 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)"); + } } From bb13165b09e1943db1c7b89ed280c61b152cd016 Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 21:02:58 +0530 Subject: [PATCH 3/6] Adding one more test case --- .../java/com/thealgorithms/physics/DampedOscillatorTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java index 29e098e1a870..9a1f42c5dddb 100644 --- a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -1,6 +1,8 @@ package com.thealgorithms.physics; -import static org.junit.jupiter.api.Assertions.*; +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; From 1debc233c420316c6ce1c3e35fdadf8b0dfbcfc5 Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 21:09:43 +0530 Subject: [PATCH 4/6] Adding one more test case --- .../physics/DampedOscillatorTest.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java index 9a1f42c5dddb..bf99afab2e01 100644 --- a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -39,15 +39,15 @@ void analyticalUnderdamped() { double gamma = 0.5; DampedOscillator d = new DampedOscillator(omega0, gamma); - double A = 1.0; + double a = 1.0; double phi = 0.2; double t = 0.123; - // expected: A * exp(-gamma * t) * cos(omega_d * t + phi) + // 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 expected = a * Math.exp(-gamma * t) * Math.cos(omegaD * t + phi); - double actual = d.displacementAnalytical(A, phi, t); + double actual = d.displacementAnalytical(a, phi, t); assertEquals(expected, actual, 1e-12, "Analytical underdamped displacement should match closed-form value"); } @@ -58,13 +58,13 @@ void analyticalOverdamped() { 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 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); + 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)"); } @@ -76,14 +76,14 @@ void eulerApproximatesAnalyticalSmallDt() { double gamma = 0.5; DampedOscillator d = new DampedOscillator(omega0, gamma); - double A = 1.0; + 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) + // 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 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 @@ -94,7 +94,7 @@ void eulerApproximatesAnalyticalSmallDt() { state = d.stepEuler(state, dt); } - double analyticAtT = d.displacementAnalytical(A, phi, tFinal); + 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. @@ -131,13 +131,13 @@ void analyticalAtZeroTime() { double gamma = 0.2; DampedOscillator d = new DampedOscillator(omega0, gamma); - double A = 2.0; + 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); + 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)"); + assertEquals(expected, actual, 1e-12, "Displacement at t=0 should be a * cos(phase)"); } } From fd4cc2739364b53cb43a31965db76ab9049f59a0 Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 21:15:03 +0530 Subject: [PATCH 5/6] Fixing build issues. --- .../java/com/thealgorithms/physics/DampedOscillatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java index bf99afab2e01..4b3e9fafe063 100644 --- a/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java +++ b/src/test/java/com/thealgorithms/physics/DampedOscillatorTest.java @@ -117,7 +117,7 @@ void eulerInputValidation() { @Test @DisplayName("Getter methods return configured parameters") void gettersReturnConfiguration() { - double omega0 = 3.14; + double omega0 = Math.PI; double gamma = 0.01; DampedOscillator d = new DampedOscillator(omega0, gamma); From dbf274e1efbd484da793c283429de636199818f7 Mon Sep 17 00:00:00 2001 From: the-yash-rajput Date: Thu, 16 Oct 2025 21:20:00 +0530 Subject: [PATCH 6/6] Fixing build issues. --- src/main/java/com/thealgorithms/physics/DampedOscillator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/thealgorithms/physics/DampedOscillator.java b/src/main/java/com/thealgorithms/physics/DampedOscillator.java index f98def45e2e1..84028b628e77 100644 --- a/src/main/java/com/thealgorithms/physics/DampedOscillator.java +++ b/src/main/java/com/thealgorithms/physics/DampedOscillator.java @@ -89,7 +89,7 @@ public double[] stepEuler(double[] state, double dt) { double x = state[0]; double v = state[1]; - double acceleration = -2.0 * gamma * v - (omega0 * omega0) * x; + double acceleration = -2.0 * gamma * v - omega0 * omega0 * x; double xNext = x + dt * v; double vNext = v + dt * acceleration;