Homework 3: NBody (Java)

A. Goals

The goal of the NBody assignment (Part I: hw02 and Part II: hw03) is to familiarize you with arrays, iteration, graphics, and Java command-line arguments. The specific goals of Part II are to:

At the end of this assignment, you will have recreated the NBody simulation in Java.

Java allows more control to the coder than Processing does, at least in reference to NBody. In Processing, the program was not able to give the user the opportunity to specify any dt, T, and filename parameters that he/she wanted. However, in your Java version of NBody, the user will be able to run the file with any dt, T, and filename parameter that he/she wants, assuming the code is done correctly.

Additionally, in the Java version of NBody, you will be able to write an explicit simulation (time) loop, instead of relying on draw() to work for you.

B. Overview

The course transitions to Java at this point. Java is a robust language heavily used in enterprise. We will spend the remainder of the semester exploring the features of Java and using Java to learn about computer science.

You will first set up your computer to develop programs in DrJava, which is a simple Java editor. You will then write a basic “Hello, world” Java program to familiarize yourself with certain new elements of Java syntax.

You will then rewrite NBody in Java. You will find that the code you will write is similar to the program you wrote in Processing for NBody: Part I; however, you will use a library called StdDraw, in lieu of Processing’s built-in drawing commands, to draw to the screen. You will also use command-line arguments and write new code to parse a simple file format.

You now have a second shot at getting the code correct. If NBody-Processing did not work as expected, take your time to make sure it works in NBody-Java.

A. Setup Java and DrJava

You must complete these steps even if you have Java already installed on your computer. The Java Runtime Environment lacks the facility to compile Java programs. Follow the instructions in this link for your computer’s operating system. The necessary tools are already installed on the machines in the Moore computer labs.

B. DrJava settings

Open DrJava, go to Edit > Preferences, and check the following settings:

C. Hello, world!

Create a folder for this assignment to save your files for this assignment. Open DrJava, and save the file as HelloWorld.java (the capitalization is important, as is the .java file extension).

Type the following program into HelloWorld.java. So that you can acquaint yourself with how DrJava indents and syntax-highlights your code as you type, do not copy and paste this program; type it out manually.

/*  Name: Eric Eaton
 *  PennKey: eeaton
 *  Recitation: 200
 *
 *  Prints "Hello, world". By tradition, this is everyone's first Java program.
 */

public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("Hello, world");
	}
}

Update the name, PennKey, and recitation number in the header comment.

In future weeks, we will explain “public class” and “public static void”; for now, this is the typical structure of a Java program.

D. Compile

Click Compile in the toolbar. If you have any syntax errors, they will appear in the Compiler Output pane at the bottom of DrJava; click on an error to highlight the relevant line, and correct the error. When there are no syntax errors, Compiler Output will display “Compilation completed.”

E. Run

Click Run in the toolbar. The Interactions pane should show “Hello, world”.

A. Using command-line arguments

Type the following program into DrJava, saving it as CommandLineArguments.java.

A Java program can take arguments when it is executed from the command-line at the Interactions pane. These arguments are passed to the main() function as a String[] array args; thus, the first command-line argument is args[0], the second is args[1], and so on.

/*  Name: Eric Eaton
 *  PennKey: eeaton
 *  Recitation: 200
 *
 *  Demonstrates command-line arguments.
 */

public class CommandLineArguments {
	public static void main(String[] args) {
		System.out.println(args.length);
		System.out.println(args[0]);
		System.out.println(Double.parseDouble(args[1])); //converts String to double
		System.out.println(Integer.parseInt(args[2])); //converts String to int
	}
}

Update the name, PennKey, and recitation number in the header comment.

B. Entering command-line arguments at the Interactions pane

Compile this program. Click Run on the toolbar. What happens? Open the Interactions pane and enter these commands at the prompt:

Make sure you understand each line of code, and why you get the output and runtime errors that you do for each set of command-line arguments.

A. main()

The first function that is run in any Java program is the main() function. Any Java file knows to automatically look at the main() function first in order to find out what to do. Java files follow the code-chronology of main(), and executes code in the order that main() tells the file to execute code. In Processing, setup() and draw() were both executed in a given .pde file. In a Java file, main() is the only function that is automatically called. If you write other functions but do not explicitly call those functions from main(), those functions will not be executed. The function header for main() is as follows:

public static void main(String[] args)

It is stylistic in Java to not include separate commands in main(). We would like to see mostly function calls in main(). The other functions that main() calls can deal with the commands that will do the "work" in this program. for-loops, while-loops, and if's are not necessarily considered commands, however, the code inside of their braces are usually commands. To figure out what functions you need, think about how you would break up your code into different actions. When drawing a car, you might want a function to draw the body and another function to draw the wheels.

B. The Different Functions

We will ask you to have five functions other than main(): readFile(), setCoordinates(), drawUniverse(), update(), and printUniverse().

Each of these functions has a singular purpose, designated by their function names. In computer science, we like to allocate different responsibilities to different functions. We like to write functions as small as possible — if a given function has more than one big responsibility, you can break that function up into two or more different functions.

Some of the functions you write might be called once and some might be called more than once. One of the advantages of a discrete function is that, if you want to use the same few lines of code in more than one place, you can call the function as many times as you want without rewriting the same lines of code every time.

You should note that each of these functions will be static functions.

There will be more specific instructions on each of these functions later, but here's a short summary of each of the functions that you must have in your submitted code.

Having written NBody once before, you should have a good understanding of how all of these functions relate to each other. Most of these functions should be called from main(), but it's possible (and legitimate) that you might want to call a function from within another function.

A. Setup

Create a new file in DrJava called NBody.java. Make sure the class name is also NBody - capitalization is important. Make sure to write your main() function, inside the curly braces for your class. Download nbody_data.zip and decompress it into the same folder as NBody.java.

B. Declaring variables

Outside main(), declare the following static global variables, being sure to use these exact names. Do not initialize any values at the global level.

C. Read command-line arguments

You can assume that every time your NBody code is run, it will be given three command-line arguments.

java NBody 1000000 1000 filename.txt

The first argument will correspond to T, the second to dt, and the third to filename.

Within main(), declare a String variable filename. Set T to the first command-line argument provided to your program, dt to the second command-line argument, and filename to the third. You may assume that your program will always be passed three command-line arguments: two doubles and one String. For instance, you might run your program as follows:

java NBody 15778000.0 25000.0 planets.txt

D. Setup readFile() and inStream

We would like you to write code to parse the file through yourself. In NBody-processing, we gave you code to do this that uses the built-in function loadStrings(filename). You are now going to write that code yourself.

Java does not have such easy built-in commands to read in from a file. Instead, the library we're going to use in this course is called stdlib. stdlib contains a sub-library called In.java with a facility to read files. Write a function called readFile() that takes a String variable location, and within it, use the following line to set up an input stream:

In inStream = new In(location);

From main(), call readFile() with the argument filename. We must create this inStream variable using the In.java library to help us parse the file. In Processing, the Strings read from a file were stored in an array using a single command. In Java, you will need to parse each piece of data in the file, one by one with multiple commands. Part F will go into more detail on how to parse the file and read each of the values.

E. Checkpoint

Compile NBody, and run it with the arguments 15778000.0, 25000.0, and planets.txt. If your program crashes, use System.out.println() statements to isolate and rectify the bug.

F. Read values

The file format of filename is the same as the file in NBody Processing (hw02, section 2D).

  1. The first value is an integer N that represents the number of particles.
  2. The second value is a real number R that represents the radius of the universe: assume all particles will have x- and y-coordinates that remain between -R and R.
  3. The remaining N rows each contain 6 values. the first is the mass (m[]); the next two are the x- and y-coordinates of the initial position (px[] and py[]); the next two are the x- and y-components of the initial velocity (vx[] and vy[]); the last is a string that is the name of an image file used to display the particle (imgNames[]). As an example, planets.txt contains data for our solar system (in SI units).
  4.         N 5
            R 2.50e+11
               5.9740e+24  1.4960e+11  0.0000e+00  0.0000e+00  2.9800e+04    earth.gif
               6.4190e+23  2.2790e+11  0.0000e+00  0.0000e+00  2.4100e+04     mars.gif
               3.3020e+23  5.7900e+10  0.0000e+00  0.0000e+00  4.7900e+04  mercury.gif
               1.9890e+30  0.0000e+00  0.0000e+00  0.0000e+00  0.0000e+00      sun.gif
               4.8690e+24  1.0820e+11  0.0000e+00  0.0000e+00  3.5000e+04    venus.gif
               m[]         px[]        py[]        vx[]        vy[]        imgNames[]
                              

Within readFile(), set N and R to the first and second values in the file. initialize the arrays to be of length N. Then, write a loop to fill in the arrays m, px, py, vx, vy, and imgNames with the values from each row. You will use some of these functions:

boolean b  = inStream.isEmpty();       // returns true if there are no more values in the file, false otherwise
int i      = inStream.readInt();       // returns the next int from inStream
double d   = inStream.readDouble();    // returns the next double from inStream
boolean b  = inStream.readBoolean();   // returns the next boolean from inStream
String s   = inStream.readString();    // returns the next String from inStream
String s   = inStream.readLine();      // returns the rest of the line from inStream
String s   = inStream.readAll();       // returns the rest of the file from inStream

inStream will start reading from the beginning of the file. Each time a function like inStream.readDouble() is called, inStream interprets the next number as a double (an error will occur if it cannot be parsed to a double). The next time a read function is called, inStream moves to the next item in the file.

For example, say that a file, “sample.txt”, is as follows:

4 5

The code snippet

In inStream = new In("sample.txt");
int x = inStream.readInt();
double y = inStream.readDouble();

will set x to 4 and y to 5.0.

G. Checkpoint

Copy and paste the following function into your file:

public static void printUniverse() {
    System.out.printf("%d\n", N);
    System.out.printf("%.2e\n", R);
    for (int i = 0; i < N; i++) {
        System.out.printf("%12.5e %12.5e %12.5e %12.5e %12.5e %12s\n",
                       m[i], px[i], py[i], vx[i], vy[i], imgNames[i]);
    }
}

Compile your program and run it with the arguments 0.0, 0.0, and planets.txt. The output from the Interactions pane should be exactly the same as the input in planets.txt. Format the function on your text-editor. Make sure to call the function at the end of main().

If your program crashes or if the output is different, use System.out.println() statements to isolate and rectify the bug. Any debugging code that you add should be commented out when you submit.

A. Background

Write a function called setCoordinates(). Within setCoordinates(), use

StdDraw.setXscale(-R, R);
StdDraw.setYscale(-R, R);

to set the coordinates of the simulation window. Call setCoordinates() from within main().

B. Drawing particles

Write a function called drawUniverse(). Within drawUniverse(), use StdDraw.picture() to draw the background starfield.jpg in the center of the StdDraw window. Write a for loop that uses StdDraw.picture() to draw each particle, in the correct position, on top of the background image. StdDraw is different from Processing and we don't need to transform px and py coordinates when displaying them on the screen. Call drawUniverse() from within main().

You can find information about StdDraw here.

C. Checkpoint

Running your sketch now using the command-line arguments 15778000.0, 25000.0, and planets.txt should show five particles (the Sun, Mercury, Venus, Earth, and Mars), in a line, stationary, on a starfield background image.

NBody still

A. Time loop

Write a loop that increments a variable t from 0 to T in steps of length dt. Your simulation should continue as long as T exceeds t. This loop goes above the debugging code provided in Section 5G. In NBody Processing, because draw() was called repeatedly, it acted as your time loop; however, in Java, this loop must be written explicitly.

Call drawUniverse() within this time loop.

B. Simulation

Write a function update(). Make gravity a static global variable g and set it to 6.67e-11 (you should initialize it in a function, but make sure it's only initialized once). The code within this function should be very similar to the code you wrote to compute the particle positions, velocities, and forces that you wrote for the NBody Processing simulation. Make sure you update velocity before updating position, and that you update position before drawing the planets to the screen. In this version of NBody, we are using doubles, not floats. Make sure to adapt your code to use doubles instead of floats.

Call update() within the time loop before your call to drawUniverse().

If your canvas is flashing, insert the following code in update(). You might need to use this line more than once (as in, in different places) if you're noticing errors in your simulation. You can read about what StdDraw does here.

StdDraw.show(30)

Feel free to look inside the StdDraw library to find out what StdDraw.show() does and how it works.

C. Checkpoint

When you run your sketch, you should see an identical animation to your NBody Processing simulation.

D. State of the Universe

Make sure to put in the printf statements when time stops, in order to print out the final state of the universe.

E. Music

Just as for NBody Part I, this task is not for credit, but it is useful for us to know for future assignments whether sound works on your computer. To add music to your simulation, add the line

StdAudio.play("2001.mid");

to setCoordinatates(). Run your sketch, and let us know in the readme (Section 8B) whether you have sound.

F. Extra Testing

Here are our results for a few sample inputs.

% With T = 25000.0 dt = 25000.0    // one step
 5
 2.50e+11
 5.97400e+24  1.49596e+11  7.45000e+08 -1.48201e+02  2.98000e+04    earth.gif
 6.41900e+23  2.27898e+11  6.02500e+08 -6.38597e+01  2.41000e+04     mars.gif
 3.30200e+23  5.78753e+10  1.19750e+09 -9.89331e+02  4.79000e+04  mercury.gif
 1.98900e+30  3.30867e+01  0.00000e+00  1.32347e-03  0.00000e+00      sun.gif
 4.86900e+24  1.08193e+11  8.75000e+08 -2.83294e+02  3.50000e+04    venus.gif
% With T = 50000.0 dt = 25000.0    // two steps
 5
 2.50e+11
 5.97400e+24  1.49589e+11  1.48998e+09 -2.96404e+02  2.97993e+04    earth.gif
 6.41900e+23  2.27895e+11  1.20500e+09 -1.27720e+02  2.40998e+04     mars.gif
 3.30200e+23  5.78258e+10  2.39449e+09 -1.97887e+03  4.78795e+04  mercury.gif
 1.98900e+30  9.92618e+01  2.81978e-01  2.64700e-03  1.12791e-05      sun.gif
 4.86900e+24  1.08179e+11  1.74994e+09 -5.66597e+02  3.49977e+04    venus.gif
% With T = 75000.0 dt = 25000.0     // three steps
 5
 2.50e+11
 5.97400e+24  1.49578e+11  2.23493e+09 -4.44605e+02  2.97978e+04    earth.gif
 6.41900e+23  2.27890e+11  1.80748e+09 -1.91579e+02  2.40995e+04     mars.gif
 3.30200e+23  5.77516e+10  3.59045e+09 -2.96820e+03  4.78386e+04  mercury.gif
 1.98900e+30  1.98523e+02  1.12801e+00  3.97047e-03  3.38411e-05      sun.gif
 4.86900e+24  1.08158e+11  2.62477e+09 -8.49891e+02  3.49931e+04    venus.gif
% With T = 31557600.0 dt = 25000.0    // one year
 5
 2.50e+11
 5.97400e+24  1.49594e+11 -1.65312e+09  3.29492e+02  2.97978e+04    earth.gif
 6.41900e+23 -2.21530e+11 -4.92633e+10  5.18050e+03 -2.36404e+04     mars.gif
 3.30200e+23  3.47713e+10  4.57516e+10 -3.82694e+04  2.94146e+04  mercury.gif
 1.98900e+30  5.94260e+05  6.23570e+06 -5.85686e-02  1.62847e-01      sun.gif
 4.86900e+24 -7.37309e+10 -7.93909e+10  2.54335e+04 -2.39734e+04    venus.gif
G. Generalization

So far, we have only been using the planets.txt file representing the solar system. However, the nbody_data.zip file you downloaded and extracted earlier contains a number of other universes. Run your program on several other universes, making certain that it works correctly in each case. If you implemented your code well (i.e., did not hard code variables), then your simulator should be capable of handling any n-body universe.

A. Style

Write a header comment for NBody.java. Ensure that you conform to the style guidelines.

B. Readme

C. Submission

Submit NBody.java and readme_nbody_java.txt on the course website.