MesaReader: The easy way to access your MESA data


What Is MesaReader?

This file gives you access to a ruby module, MesaReader, containing three classes: MesaData, MesaProfileIndex, and MesaLogDir. The primary use is intended for plotting in Tioga, but as the tools become more sophisticated, analysis can be done on the fly with irb or standalone scripts.

Installation

Prerequisites

Obivously you need an installation of Ruby. I haven't found any conflicts with Ruby 1.8.7, 1.9.3, or 2.0+, and I'm told it now works with 1.8.6. If you are doing a manual installation, you will also need to have tiogga, a plotting package, installed since MesaReader gathers data into Dvectors, which are a part of Tioga.

Quick Installation

As of this writing, MesaReader is now packaged as a gem. If you have RubyGems working (and you probably do), you should be able to install it via

gem install mesa_reader

You may need to have superuser privileges, so just prepend sudo to the previous command if rubygems barks at you about privileges. Tioga is a prerequisite since MesaReader loads data into Dvectors, but Tioga installation should occur automatically.

Manual Installation

If you can't use a RubyGems installation, you can manually place the mesa_reader.rb file in useful location for installation. Copying it to your work directory may be sufficient if you just plan to use this for plotting purposes since it will copy over with the rest of the directory whenever you make a new work directory.

Another possible installation idea that I've been using is to clone the git repository somewhere sensible on my machine and then create a pointer file in my Ruby path (see below). For instance, I have a file in /usr/lib/ruby/ called mesa_reader.rb that has one line:

require '/Users/wmwolf/Documents/MESA_Reader/mesa_reader/lib/mesa_reader.rb'

That way, the source file is easily accessible in my git repo while also being readable from Ruby's path. That is, when you tell ruby to require MesaReader, it will find the file in its path, which will then load the file from your local repo.

To clone the git repo on your machine (assuming you have git installed), cd to the directory where you want it installed and clone it. You'll want to change the path to where you want to install the file, but the commands are

cd ~/Documents
git clone https://github.com/wmwolf/MESA_Reader.git MesaReader

which will install the contents of the repo (currently a RubyGems-style directory for MesaReader and this README) into a directory called MesaReader in your Documents directory. Then any time you want to update to the latest release, you can do

cd ~/Documents/MesaReader
git pull

Or, hopefully, you can make your own additions and push updates for me to add!

Global Installation

For a more permanent solution, put mesa_reader.rb in your ruby path. For me (on a mac), placing it in /usr/lib/ruby/ did the trick. To see where you should install the file for global use enter the following line into your terminal

ruby -e 'puts $:'

This should give you a list of directories that would work for installation. To update to a newer version of these tools, just move the new file to the same place.

Making the Module Available

In your program (or irb), just start with

require 'mesa_reader'

If the file is in the same directory as your current working one, it will be read from there. Otherwise, ruby will search through its available paths for a file called mesa_reader.rb and load the first one it finds. Note that in Ruby 2.0+, the working directory is no longer on the search. You must either use require_relative or explicitly indicate that you want to search the current directory with require './mesa_reader. This will also work in earlier versions of ruby, so if you are requiring a local copy, you should get in this habit.

If you want direct access to the classes, you will also need to put in the line

include MesaReader

This takes all of the contents of the module MesaReader and drops them into your current namespace. If you don't do this, you'll just have to add MesaReader:: in front of any class name, like MesaReader::MesaData or MesaReader::MesaLogDir. For subsequent examples, I'll assume you have included the entire module (i.e., no more typing out MesaReader::).

Creating Instances

To create a simple MesaData instance, use the class #initialize method:

s = MesaData.new(FILEPATH)

where FILEPATH is a string that is the path (relative or fully-qualified) to the file you wish to read in. For instance, if you are in the work directory and you want to read in the history file, you would use 'LOGS/history.data' in place of FILEPATH. You can load history or profile files since the basic MesaData object doesn't know the difference (though when loading history files, it does know to throw out backups, retries, and restarts, ensuring that the model numbers are monotonically increasing).

To create a MesaProfileIndex instance, we'll use that class' #initialize method as well.

m = MesaProfileIndex.new(FILEPATH)

where now FILEPATH is a string containing the path to your profiles index file, like 'LOGS/profiles.index'.

Finally, to make a MesaLogDir instance, we use that class' #initialize method again. Unlike the first two examples, though, this class can take many more initialization parameters. To use just two of them, here's an example:

l = MesaLogDir.new('log_path' => '~/mesa/star/work/LOGS', 'history_file' => 'history.data')

You can also set 'profile_prefix', 'profile_suffix', and 'index_file', which denote the part of the name of profile before the number, the suffix of a profile file, and the full name of the index file. The defaults are

'log_path'       => 'LOGS'
'profile_prefix' => 'profile'
'profile_suffix' => 'data'
'history_file'   => 'history.data'
'index_file'     => 'profiles.index

Normally these shouldn't need to be altered, and so long as you don't have custom-named log directories, l = MesaLogDir.new, with no options, should suffice.

MesaData Methods

There are many publicly accessible instance methods for MesaData objects. Their usage is detailed below. For convenience, we'll assume that s is an instance of the MesaData class. That is, assume we have already done

s = MesaData.new('LOGS/history.data')

#bulk_data

Returns an array of Dvectors containing the columns from the source file. This isn't very useful and is really only a diagnostic tool.

#bulk_names

Returns an array of strings that are the names of each of the data columns. These are the strings at the top of each data column from the source file. Together with #bulk_data forms a hash that is accessed using the #data command to convert data column names into Dvectors.

#data(key)

Accepts a string and returns the corresponding Dvector from #bulk_data. This is the main usage of the class.

s.data('model_number') => [1.0, 2.0, 3.0, 4.0, ...]

Returns nil and prints a warning if no such key exists in s.bulk_names.

#data_at_model_number(key, model_number)

Accepts a string and a model number (float or integer) and returns the value of s.data(key) at the index corresponding to the given model number. If no such data category exists, returns nil and prints a warning. An exception will be thrown if model_number is not a data category or if the given model number is outside the range of s.data('model_number').

#data?(key)

Accepts a string and returns true if the entry is found in s.bulk_names and false otherwise.

#file_name

Returns a string containing the name of the file that was read into the instance.

#header(key)

Accepts a string and returns the corresponding value from #header_data. The key value must be in s.header_names or else it will return nil (and a warning will be printed). Works in much the same way as #data but with the header data rather than the bulk data.

#header?(key)

Accepts a string and returns true if the string is in s.header_names.

#header_data

Returns an array of all the data in the header row of the source file.

#header_names

Returns an array of strings containing all the names of the header data entries.

#where(keys)

Accepts an arbitrary (at least one) number of strings as arguments, each of which must be a member of s.bulk_names. Then yields each member of s.data(key1), s.data(key2), etc. into a [required] block and performs a user-specified test on those data. Returns an array of integers containing the indices of the set members that passed the test. For example

s.where('star_age', 'log_L') { |age, lum| age > 1e6 and lum > 2 }

returns an array containing the indices , i such that s.data('star_age')[i] > 1e6 and s.data('log_L')[i] > 2. A common usage would then be to feed these indices back in to get a subset of an array from s.data('star_age'). For instance, one could get all the values of the luminosity for times later than a million years via

s.data('luminosity').values_at(*s.where('star_age) { |age| age > 1e6 })

Magic Methods

Depending on the type of file read in to the object (specifically what ends up in s.bulk_names), you can also access the bulk data through some shorthand without using the #data method. So long as the data name isn't already a defined method on the MesaData class, you can simply use its name as a method which just returns data("#{name}"). That is, if you load in a history file,

s.data('model_number')

and

s.model_number

should return the same thing. This is essentially just syntatic sugar, but it makes for more readable code. The #data way of doing things is always invoked at some point, so it is the "preferred" method of accessing data, but magic methods should, for nearly all cases, perform just as well unless you have unfortunately named data categories (starting with numbers, or they are tragically named the same as an already existing method, like, nil?).

If the method name used is not in s.bulk_names but is in s.header_names, then it returns the appropriate header data instead. If the name is in both the header and data names, the data entry wins (this sometimes happens with things like version_number and the like). If you use an invalid method (i.e. one that is not explicitly defined in the class or its superclass, the basic Ruby object Object, or a method implicitly defined from the data and header categories via magic methods) a NameError will be thrown, just like it is for any other case of a bad method call.

MesaProfileIndex Methods

There are five publicly accessible methods for the MesaProfileIndex class, though likely the only useful methods are #have_profile_with_model_number? and #profile_with_model_number, which allow you to obtain a model number from a model number. In practice, this class isn't very useful on its own, but is used extensively in the MesaLogDir class.

#have_profile_with_model_number?(model_number)

Accepts an integer, a model number, and returns true if there is a profile available with that model number. Otherwise returns false.

#have_profile_with_profile_number?(profile_number)

Accepts an integer, a profile number, and returns true if there is a profile with that profile number. Otherwise returns false.

#model_numbers

Returns a Dvector containing all the model numbers that have profiles available.

#profile_numbers

Returns a Dvector containing all the profile numbers available.

#profile_with_model_number(model_number)

Accepts an integer model number and returns the profile number that corresponds to it. Returns nil if there is no such profile.

MesaLogDir Methods

We'll suppose we've already made an instance for the purpose of examples via

l = MesaLogDir.new

In addition to the methods defined below, all the methods of MesaProfileIndex are available and are simply called on the internal MesaProfileIndex created within the MesaLogDir structure.

#contents

Returns an array of strings containing names of all the files in the directory returned by l.log_path.

#history_data

Returns a MesaData instance made from l.history_file in l.log_path. This object is created at initialization and is thus "free". There's no need to catch this in a variable to spare the MesaData initialization process each time it is called.

#history

Alias for history.

#history_file

Returns the name of the history data file in l.log_path.

#index_file

Returns the name of the profile index file in l.log_path.

#log_path

Returns a string containing the given path to the logs directory.

#profiles

Returns a MesaProfileIndex instance built from l.index_file

#profile_data(params)

Accepts two possible integer arguments, 'model_number' or 'profile_number' which specify a profile to be loaded. If neither are given, the profile with the largest model number (i.e., the last saved profile) is selected. Returns a MesaData object built from this profile. If the model number provided has no profile (i.e. l.profiles.profile_with_model_number(params['model_number']) => nil), then the default model number is selected. If a profile number is used, it will attempt to use it no matter what, triggering an error if the given profile number is invalid. For example

p = l.profile_data('model_number' => 300)

would set p to be a MesaData object built from the profile data associated with model number 300. If no such model number existed, though, it would pull data from the profile with the largest model number. If we instead used

p = l.profile_data('model_number' => 300, 'profile_number' => 15)

then p would be set to a MesaData object with profile number 15. The 'model_number' entry is entirely ignored. If there was no profile with profile number 15, an exception will be raised. Essentially there is never a time when it is helpful to specify both a model number and a profile number, and again, if neither are specified, the profile with the largest model number is used.

Each of these objects are made as this is called. That is, they aren't "sitting around" like the history MesaData object. As such, these should be captured in a variable so that they aren't re-constructed each time they are needed.

#profile_prefix

Returns the string containing the profile prefix as defined in the MesaProfileIndex class.

#profile_suffix

Returns the string containing the profile suffix as defined in the MesaProfileIndex class.

#select_models(keys)

Nearly identical to #where in the MesaData class, but ensures that the returned model numbers have corresponding profile files. Accepts an arbitrary number (at least one) of strings that must be in l.history_data.bulk_names and yields successive values of l.history_data.data(key) for each key to a user-specified block that should return a boolean. Only those model numbers that have available profiles are tested, so the returned Dvector of model numbers (not indices, like in #where) have available profiles and pass the test provided by the user. As an example

models_to_plot = l.select_models('log_center_T', 'log_center_Rho') { |log_tc, log_rhoc| log_tc > 8 and log_rhoc > 3 }

will return a Dvector of model numbers that have profiles available for reading in and have central temperatures exceeding 1e8 and have central densities exceeding 1e3.

Some Additional Thoughts

The uses for these classes are pretty generic. As stated earlier, they were developed primarily to ease plotting MESA data in Tioga, but they are also quite useful for manipulating the data in their own rite for numerical purposes. The only reason you might not want to do that is that Ruby isn't the fastest language available, but then again, if you are dealing with such large MESA data sets that the computational timescales are getting too long for your comfort, you are in a pretty remarkable situation. As a practical note, if you use this in irb and you make an instance of MesaData, I'd recommend following that up with a semi-colon and nil unless you want to see a ton of numbers fly up your screen. For instance, do this

l = MesaLogDir.new; nil

The nil keeps irb from outputting all the data held in l. Or consider using the wonderful irb replacement Pry which can make exploring your data outside of plots a much more pleasant experience (for instance a simple semi-colon will suppress outputting the return value). If you have any problems with or suggestions for further development of these classes, please contact me or better yet, make some commits and push the changes for deployment!