Skip to content

Commit 8930369

Browse files
Added SimplePendulumRK4 (TheAlgorithms#6800)
* Added SimplePendulumRK4 * Fixed build issue. * Fixed build issue. * Fixed build issue.
1 parent 873dd97 commit 8930369

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package com.thealgorithms.physics;
2+
3+
/**
4+
* Simulates a simple pendulum using the Runge-Kutta 4th order method.
5+
* The pendulum is modeled with the nonlinear differential equation.
6+
*
7+
* @author [Yash Rajput](https://github.com/the-yash-rajput)
8+
*/
9+
public final class SimplePendulumRK4 {
10+
11+
private SimplePendulumRK4() {
12+
throw new AssertionError("No instances.");
13+
}
14+
15+
private final double length; // meters
16+
private final double g; // acceleration due to gravity (m/s^2)
17+
18+
/**
19+
* Constructs a simple pendulum simulator.
20+
*
21+
* @param length the length of the pendulum in meters
22+
* @param g the acceleration due to gravity in m/s^2
23+
*/
24+
public SimplePendulumRK4(double length, double g) {
25+
if (length <= 0) {
26+
throw new IllegalArgumentException("Length must be positive");
27+
}
28+
if (g <= 0) {
29+
throw new IllegalArgumentException("Gravity must be positive");
30+
}
31+
this.length = length;
32+
this.g = g;
33+
}
34+
35+
/**
36+
* Computes the derivatives of the state vector.
37+
* State: [theta, omega] where theta is angle and omega is angular velocity.
38+
*
39+
* @param state the current state [theta, omega]
40+
* @return the derivatives [dtheta/dt, domega/dt]
41+
*/
42+
private double[] derivatives(double[] state) {
43+
double theta = state[0];
44+
double omega = state[1];
45+
double dtheta = omega;
46+
double domega = -(g / length) * Math.sin(theta);
47+
return new double[] {dtheta, domega};
48+
}
49+
50+
/**
51+
* Performs one time step using the RK4 method.
52+
*
53+
* @param state the current state [theta, omega]
54+
* @param dt the time step size
55+
* @return the new state after time dt
56+
*/
57+
public double[] stepRK4(double[] state, double dt) {
58+
if (state == null || state.length != 2) {
59+
throw new IllegalArgumentException("State must be array of length 2");
60+
}
61+
if (dt <= 0) {
62+
throw new IllegalArgumentException("Time step must be positive");
63+
}
64+
65+
double[] k1 = derivatives(state);
66+
double[] s2 = new double[] {state[0] + 0.5 * dt * k1[0], state[1] + 0.5 * dt * k1[1]};
67+
68+
double[] k2 = derivatives(s2);
69+
double[] s3 = new double[] {state[0] + 0.5 * dt * k2[0], state[1] + 0.5 * dt * k2[1]};
70+
71+
double[] k3 = derivatives(s3);
72+
double[] s4 = new double[] {state[0] + dt * k3[0], state[1] + dt * k3[1]};
73+
74+
double[] k4 = derivatives(s4);
75+
76+
double thetaNext = state[0] + dt / 6.0 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]);
77+
double omegaNext = state[1] + dt / 6.0 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]);
78+
79+
return new double[] {thetaNext, omegaNext};
80+
}
81+
82+
/**
83+
* Simulates the pendulum for a given duration.
84+
*
85+
* @param initialState the initial state [theta, omega]
86+
* @param dt the time step size
87+
* @param steps the number of steps to simulate
88+
* @return array of states at each step
89+
*/
90+
public double[][] simulate(double[] initialState, double dt, int steps) {
91+
double[][] trajectory = new double[steps + 1][2];
92+
trajectory[0] = initialState.clone();
93+
94+
double[] currentState = initialState.clone();
95+
for (int i = 1; i <= steps; i++) {
96+
currentState = stepRK4(currentState, dt);
97+
trajectory[i] = currentState.clone();
98+
}
99+
100+
return trajectory;
101+
}
102+
103+
/**
104+
* Calculates the total energy of the pendulum.
105+
* E = (1/2) * m * L^2 * omega^2 + m * g * L * (1 - cos(theta))
106+
* We use m = 1 for simplicity.
107+
*
108+
* @param state the current state [theta, omega]
109+
* @return the total energy
110+
*/
111+
public double calculateEnergy(double[] state) {
112+
double theta = state[0];
113+
double omega = state[1];
114+
double kineticEnergy = 0.5 * length * length * omega * omega;
115+
double potentialEnergy = g * length * (1 - Math.cos(theta));
116+
return kineticEnergy + potentialEnergy;
117+
}
118+
119+
public double getLength() {
120+
return length;
121+
}
122+
123+
public double getGravity() {
124+
return g;
125+
}
126+
}
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package com.thealgorithms.physics;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Test;
6+
7+
/**
8+
* Test class for SimplePendulumRK4.
9+
* Tests numerical accuracy, physical correctness, and edge cases.
10+
*/
11+
class SimplePendulumRK4Test {
12+
13+
private static final double EPSILON = 1e-6;
14+
private static final double ENERGY_DRIFT_TOLERANCE = 1e-3;
15+
@Test
16+
@DisplayName("Test constructor creates valid pendulum")
17+
void testConstructor() {
18+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.5, 9.81);
19+
Assertions.assertNotNull(pendulum);
20+
Assertions.assertEquals(1.5, pendulum.getLength(), EPSILON);
21+
Assertions.assertEquals(9.81, pendulum.getGravity(), EPSILON);
22+
}
23+
24+
@Test
25+
@DisplayName("Test constructor rejects negative length")
26+
void testConstructorNegativeLength() {
27+
Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(-1.0, 9.81); });
28+
}
29+
30+
@Test
31+
@DisplayName("Test constructor rejects negative gravity")
32+
void testConstructorNegativeGravity() {
33+
Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(1.0, -9.81); });
34+
}
35+
36+
@Test
37+
@DisplayName("Test constructor rejects zero length")
38+
void testConstructorZeroLength() {
39+
Assertions.assertThrows(IllegalArgumentException.class, () -> { new SimplePendulumRK4(0.0, 9.81); });
40+
}
41+
42+
@Test
43+
@DisplayName("Test getters return correct values")
44+
void testGetters() {
45+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(2.5, 10.0);
46+
Assertions.assertEquals(2.5, pendulum.getLength(), EPSILON);
47+
Assertions.assertEquals(10.0, pendulum.getGravity(), EPSILON);
48+
}
49+
50+
@Test
51+
@DisplayName("Test single RK4 step returns valid state")
52+
void testSingleStep() {
53+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
54+
double[] state = {0.1, 0.0};
55+
double[] newState = pendulum.stepRK4(state, 0.01);
56+
57+
Assertions.assertNotNull(newState);
58+
Assertions.assertEquals(2, newState.length);
59+
}
60+
61+
@Test
62+
@DisplayName("Test equilibrium stability (pendulum at rest stays at rest)")
63+
void testEquilibrium() {
64+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
65+
double[] state = {0.0, 0.0};
66+
67+
for (int i = 0; i < 100; i++) {
68+
state = pendulum.stepRK4(state, 0.01);
69+
}
70+
71+
Assertions.assertEquals(0.0, state[0], EPSILON, "Theta should remain at equilibrium");
72+
Assertions.assertEquals(0.0, state[1], EPSILON, "Omega should remain zero");
73+
}
74+
75+
@Test
76+
@DisplayName("Test small angle oscillation returns to initial position")
77+
void testSmallAngleOscillation() {
78+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
79+
double initialAngle = Math.toRadians(5.0);
80+
double[] state = {initialAngle, 0.0};
81+
double dt = 0.01;
82+
83+
// Theoretical period for small angles
84+
double expectedPeriod = 2 * Math.PI * Math.sqrt(1.0 / 9.81);
85+
int stepsPerPeriod = (int) (expectedPeriod / dt);
86+
87+
double[][] trajectory = pendulum.simulate(state, dt, stepsPerPeriod);
88+
double finalTheta = trajectory[stepsPerPeriod][0];
89+
90+
// After one period, should return close to initial position
91+
double error = Math.abs(finalTheta - initialAngle) / Math.abs(initialAngle);
92+
Assertions.assertTrue(error < 0.05, "Small angle approximation error should be < 5%");
93+
}
94+
95+
@Test
96+
@DisplayName("Test large angle oscillation is symmetric")
97+
void testLargeAngleOscillation() {
98+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
99+
double[] state = {Math.toRadians(120.0), 0.0};
100+
101+
double[][] trajectory = pendulum.simulate(state, 0.01, 500);
102+
103+
double maxTheta = Double.NEGATIVE_INFINITY;
104+
double minTheta = Double.POSITIVE_INFINITY;
105+
106+
for (double[] s : trajectory) {
107+
maxTheta = Math.max(maxTheta, s[0]);
108+
minTheta = Math.min(minTheta, s[0]);
109+
}
110+
111+
Assertions.assertTrue(maxTheta > 0, "Should have positive excursions");
112+
Assertions.assertTrue(minTheta < 0, "Should have negative excursions");
113+
114+
// Check symmetry
115+
double asymmetry = Math.abs((maxTheta + minTheta) / maxTheta);
116+
Assertions.assertTrue(asymmetry < 0.1, "Oscillation should be symmetric");
117+
}
118+
119+
@Test
120+
@DisplayName("Test energy conservation for small angle")
121+
void testEnergyConservationSmallAngle() {
122+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
123+
double[] state = {Math.toRadians(15.0), 0.0};
124+
125+
double initialEnergy = pendulum.calculateEnergy(state);
126+
127+
for (int i = 0; i < 1000; i++) {
128+
state = pendulum.stepRK4(state, 0.01);
129+
}
130+
131+
double finalEnergy = pendulum.calculateEnergy(state);
132+
double drift = Math.abs(finalEnergy - initialEnergy) / initialEnergy;
133+
134+
Assertions.assertTrue(drift < ENERGY_DRIFT_TOLERANCE, "Energy drift should be < 0.1%, got: " + (drift * 100) + "%");
135+
}
136+
137+
@Test
138+
@DisplayName("Test energy conservation for large angle")
139+
void testEnergyConservationLargeAngle() {
140+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
141+
double[] state = {Math.toRadians(90.0), 0.0};
142+
143+
double initialEnergy = pendulum.calculateEnergy(state);
144+
145+
for (int i = 0; i < 1000; i++) {
146+
state = pendulum.stepRK4(state, 0.01);
147+
}
148+
149+
double finalEnergy = pendulum.calculateEnergy(state);
150+
double drift = Math.abs(finalEnergy - initialEnergy) / initialEnergy;
151+
152+
Assertions.assertTrue(drift < ENERGY_DRIFT_TOLERANCE, "Energy drift should be < 0.1%, got: " + (drift * 100) + "%");
153+
}
154+
155+
@Test
156+
@DisplayName("Test simulate method returns correct trajectory")
157+
void testSimulate() {
158+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
159+
double[] initialState = {Math.toRadians(20.0), 0.0};
160+
int steps = 100;
161+
162+
double[][] trajectory = pendulum.simulate(initialState, 0.01, steps);
163+
164+
Assertions.assertEquals(steps + 1, trajectory.length, "Trajectory should have steps + 1 entries");
165+
Assertions.assertArrayEquals(initialState, trajectory[0], EPSILON, "First entry should match initial state");
166+
167+
// Verify state changes over time
168+
boolean changed = false;
169+
for (int i = 1; i <= steps; i++) {
170+
if (Math.abs(trajectory[i][0] - initialState[0]) > EPSILON) {
171+
changed = true;
172+
break;
173+
}
174+
}
175+
Assertions.assertTrue(changed, "Simulation should progress from initial state");
176+
}
177+
178+
@Test
179+
@DisplayName("Test energy calculation at equilibrium")
180+
void testEnergyAtEquilibrium() {
181+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
182+
double[] state = {0.0, 0.0};
183+
double energy = pendulum.calculateEnergy(state);
184+
Assertions.assertEquals(0.0, energy, EPSILON, "Energy at equilibrium should be zero");
185+
}
186+
187+
@Test
188+
@DisplayName("Test energy calculation at maximum angle")
189+
void testEnergyAtMaxAngle() {
190+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
191+
double[] state = {Math.PI / 2, 0.0};
192+
double energy = pendulum.calculateEnergy(state);
193+
Assertions.assertTrue(energy > 0, "Energy should be positive at max angle");
194+
}
195+
196+
@Test
197+
@DisplayName("Test energy calculation with angular velocity")
198+
void testEnergyWithVelocity() {
199+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
200+
double[] state = {0.0, 1.0};
201+
double energy = pendulum.calculateEnergy(state);
202+
Assertions.assertTrue(energy > 0, "Energy should be positive with velocity");
203+
}
204+
205+
@Test
206+
@DisplayName("Test stepRK4 rejects null state")
207+
void testStepRejectsNullState() {
208+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
209+
Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(null, 0.01); });
210+
}
211+
212+
@Test
213+
@DisplayName("Test stepRK4 rejects invalid state length")
214+
void testStepRejectsInvalidStateLength() {
215+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
216+
Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(new double[] {0.1}, 0.01); });
217+
}
218+
219+
@Test
220+
@DisplayName("Test stepRK4 rejects negative time step")
221+
void testStepRejectsNegativeTimeStep() {
222+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
223+
Assertions.assertThrows(IllegalArgumentException.class, () -> { pendulum.stepRK4(new double[] {0.1, 0.2}, -0.01); });
224+
}
225+
226+
@Test
227+
@DisplayName("Test extreme condition: very large angle")
228+
void testExtremeLargeAngle() {
229+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
230+
double[] state = {Math.toRadians(179.0), 0.0};
231+
double[] result = pendulum.stepRK4(state, 0.01);
232+
233+
Assertions.assertNotNull(result);
234+
Assertions.assertTrue(Double.isFinite(result[0]), "Should handle large angles without NaN");
235+
Assertions.assertTrue(Double.isFinite(result[1]), "Should handle large angles without NaN");
236+
}
237+
238+
@Test
239+
@DisplayName("Test extreme condition: high angular velocity")
240+
void testExtremeHighVelocity() {
241+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
242+
double[] state = {0.0, 10.0};
243+
double[] result = pendulum.stepRK4(state, 0.01);
244+
245+
Assertions.assertNotNull(result);
246+
Assertions.assertTrue(Double.isFinite(result[0]), "Should handle high velocity without NaN");
247+
Assertions.assertTrue(Double.isFinite(result[1]), "Should handle high velocity without NaN");
248+
}
249+
250+
@Test
251+
@DisplayName("Test extreme condition: very small time step")
252+
void testExtremeSmallTimeStep() {
253+
SimplePendulumRK4 pendulum = new SimplePendulumRK4(1.0, 9.81);
254+
double[] state = {Math.toRadians(10.0), 0.0};
255+
double[] result = pendulum.stepRK4(state, 1e-6);
256+
257+
Assertions.assertNotNull(result);
258+
Assertions.assertTrue(Double.isFinite(result[0]), "Should handle small time steps without NaN");
259+
Assertions.assertTrue(Double.isFinite(result[1]), "Should handle small time steps without NaN");
260+
}
261+
}

0 commit comments

Comments
 (0)