227 Solar System

Description

Kaltxì Rubyists,

This week’s quiz is to create a solar system simulator. It will take a date as input then return a list of the distances to each planet from Earth at that date.

Have fun!

Summary

There were two solutions for this quiz, a mathematical computation approach, and a web services based approach.

Glen F. Pankow used a mathematical approach from the NASA Horizons site as described in Formulae for Computing Approximate Planetary Positions. Glen’s program makes use of a table of data containing the Keplerian elements of each planetary body. Then when given the date in question as input, the numbers are crunched to produce the x, y, and z positions of each planetary body at that point in time. The final step is to compute the distance from the position of the Earth to each of those positions and display the results.

The mathematical formulas involved in computing the positions can be daunting, so a tip of the hat to Glen for writing them up with good comments in a step by step solution. Glen’s solution is certainly worth taking a look at if you are interested in the mathematics of planetary motion.

Jean Lazarou provided a web-services based solution that queried the NASA Horizons system directly as well as a summary of the solution.

The following section of the summary is written by Jean Lazarou.

After asking a friend, who pointed me to the NASA horizons system site which provides solar system data, I realised that a simple solution would be to use the web version of the NASA’s site tool to retrieve the distances.

Understanding a site…

After playing a little bit with horizons I was able to use it to retrieve interactively the distance from earth to any planet of our solar system at a given date.

The user sets an amount of parameters first that define some query and then he/she asks for the result.

Horizons seems to store in a HTTP session the parameters that define what data we want to retrieve. It has several group of parameters like the result format, the target planet, the origin planet, etc. Each group may contain several parameters. Because we can query only one planet at a time, we must submit eight queries to retrieve all the distances.

To implement the solution we must find how our program can communicate with the server. Using a Firefox addon, HttpFox for instance, we can analyse the underlying HTTP data. We record all the necessary postings, then we locate the values we need to change (the date and the planets). To retrieve the results we need, our program parse the response returned by the server. As Horizons can return simple text, parsing the result is easier.

The horizons web application expects post requests using form-data as multipart format. Because the standard HTTP layer in Ruby does not support multipart data transfer, we are going to add a simple support for it.

Multipart form-data

Before presenting how we can add multipart support, let us first see one way of using the HTTP library (using Ruby 1.8).

Posting data with Ruby’s HTTP library

Next snippet is a quick overview of a post request using the HTTP library.

01 require 'net/http'
02 
03 server = 'mys_server.com'
04 port = '8080'
05 
06 http = Net::HTTP.new(server, port)
07 
08 path = '/submit'
09 content = 'name=me'
10 headers = {''Content-Type' => 'application/x-www-form-urlencoded'}
11 
12 response, body = http.post(path, content, headers)
13 

At line 6 we create an HTTP object. At line 12 we send a POST request with one parameter.

We could write the same code using other APIs from the HTTP library, we wrote it this way as a base for what follows.

Extending the default library

We are not going to extend the classes used by the HTTP library, but specifically add the multipart support to the objects we create. We define a kind of factory method named create_http_with_multipart.

01 require 'net/http'
02 
03 def create_http_with_multipart server, port = nil
04 
05   http = Net::HTTP.new(server, port)
06   
07   def http.post_multipart path, parameters, headers = {}
08   end
09   
10   http
11   
12 end

Line 10 returns a normal HTTP object with one more method named post_multipart added to the new HTTP object at line 7.

The post_multipart method expects the server path for the request, a hash object with the parameters and a hash object with the optional header parameters. (It does not support files as parameters.)

01   def http.post_multipart path, parameters, headers = {}
02   
03     boundary = 'xxboundaryxx'
04     
05     data = []
06     
07     parameters.each do |name, value|
08       data << "--#{boundary}"
09       data << "Content-Disposition: form-data; name=\"#{name}\""
10       data << ''
11       data << value.to_s
12     end
13     
14     data << "--#{boundary}"
15     data << ''
16 
17     body = data.join("\r\n")
18 
19     headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
20     headers['Content-Length'] = body.length.to_s
21     
22     return post(path, body, headers)
23     
24   end

The implementation loops through all the parameters (line 7) filling an array (data) and adding each time a new part (starting with the boundary sequence – line 8). It adds a last boundary after the loop, before converting the array to a string (lines ended with carriage return / line feed – line 17).

Then it adds the necessary headers, notice the one setting the content type with the boundary string (line 19).

And finally it calls the inherited post method (line 22).

Back to distance…

We hide the complexity of the horizons site communication in the Horizons class. It initializes the parameters in the server’s session, stores the necessary cookie, makes the eight requests and parses the result. Parsing the result is very simple as the horizons encloses the data table between two lines containing $$SOE and $$EOE.

The main script uses the Horizons class and loops waiting for user to input dates.

Conclusion

The result is rather slow (mainly because we need to make eight requests).

We didn’t really solve the problem. But we didn’t reinvent the wheel, was it possible? ;)

Here is a run result, so that you can check the planet positions on the 2nd of May (distances may seem short but they are expressed in AU – astronomical unit).

$ ruby solar.rb 
Enter a date or nothing to quit
Date (YYYY-MM-DD): 2010-05-02
  JUPITER: 5.60264666760831 AU
  VENUS: 1.45723642739716 AU
  SATURN: 8.75025556531919 AU
  MERCURY: 0.56158052050790 AU
  URANUS: 20.8265012641112 AU
  PLUTO: 31.2222615921462 AU
  MARS: 1.29568386653263 AU
  NEPTUNE: 30.2996308594089 AU

  (1 AU = 149,597,870.691 km)
Date (YYYY-MM-DD):
$

Solar System (#227) - Solutions


Saturday, January 16, 2010