###MESA Requirement!
In its current state, MesaScript requires MESA rev. 5596 or above. This is due
to a sensitivity to where the '.inc'
files are stored on earlier versions. If
there is demand for MesaScript for earlier revisions, I will look into making
it backward compatible.
###The Short Short Version
To get up and running fast, skip to installation, then try and use the included
sample file, sample.rb
(via running ruby sample.rb
in the command line). The
comments in sample.rb
should get you started, especially if you have at least
a little Ruby know-how.
###What is MesaScript?
Lightweight, Ruby-based language that provides a powerful way to make inlists
for MESA projects. Really, MesaScript is a DSL (domain-specific language) built
on top of Ruby, so Ruby code "just works" inside MesaScript (if you're familiar
with it, think of what SASS is to CSS, but on a smaller scale).
MesaScript provides a way to build inlists for use with
MESA using Ruby, though you need not know much
at all about Ruby to use it. The main point is that you can use
variables when creating an inlist, making a reusable template for parameter
space studies when only a few inlist commands vary between a large number of
inlists. Most, if not all, of what MesaScript does can be done by using MESA's
run_star_extras
hooks, but for the purposes of documenting what I do with
MESA, I find inlists more enlightening, and I try to stick to high-level
languages whenever I can.
There are other benefits, too. MesaScript automatically checks your input to make sure that the types of arguments you give for various namelist items match what is expected, and the resulting inlist is neatly formatted and sensibly ordered. You can also easily convert an existing inlist to MesaScript for editing and further generalization. In general, writing an inlist in MesaScript is no more difficult than writing a normal inlist, but you have far more flexibility. So why not give it a try?
If you know a little Ruby (want to learn? Try Ruby here!), the possibilities are pretty wide open. You could easily make a script that starts with a given set of parameters, run MESA star, then use the output of that run to dictate a new inlist and run, creating a chain (maybe a MESA root find of sorts).
###Installation
MesaScript is now available as a gem! Assuming you have the gem
command up
and running (you probably do, but if not, check out
RubyGems to get it up and running). Simply run
gem install mesa_script
and you should be good to go. You'll be able to include MesaScript in your
ruby files with require mesa_script
, and inlist2mesascript
will be
available to you from the command line to convert your existing inlists to
mesascript files.
If you want to edit the source or don't want to use rubygems, clone or otherwise download the repository somewhere to your home directory with
git clone https://github.com/wmwolf/MesaScript.git ~/MesaScript
or somewhere else to your liking.
Then, either copy the file mesa_script.rb
to somewhere along Ruby's path, or
set up another stand-in file that points to your mesa_script.rb
file in
Ruby's path. To find Ruby's path, type
ruby -e 'puts $:'
in your terminal. If Ruby is properly configured, as it is on most modern Unix
systems, you should see a list of possible directories. Either copy
mesa_script.rb
there or do what I do and make a new file called
mesa_script.rb
there and have it just be
require '/PATH/TO/YOUR/CLONED/REPOSITORY/mesa_script.rb'
This way, if you later update your repo via git pull
, you won't need to copy
mesa_script.rb
again. Also, if you'd like to use the included (optional)
inlist2mesascript
tool, copy that to somewhere along you system's path (
echo $PATH
). Then type inlist2mesascript -h
to learn more about that tool.
As you
might guess, it takes an existing MESA inlist and converts it to a file in
MesaScript that, if executed by Ruby should produce essentially the same inlist
(good for moving a project to MesaScript).
To check if Ruby can see the file, try doing ruby -e 'require "mesa_script"'
.
If no error occurs, it is working fine.
Finally, you must have your MESA_DIR
environment variable set for anything to
work. The mesa_script.rb
file generates all the necessary data it needs from
the MESA source on the fly (this also makes it nearly MESA version
independent).
###Basic Usage
The mesa_script.rb
file defines just one class, Inlist, which we'll interact
with primarily through one class method, make_inlist
. Just put the following
in a file to make a blank inlist:
require 'mesa_script'
Inlist.make_inlist('babys_first_inlist') {
# inlist commands go here
}
This creates a file called babys_first_inlist
that will be pretty
boring. It will create three namelists (the usual star_job
, controls
, and
pgstar
) and leaves them blank inside, which is a perfectly acceptable inlist
for MESA to use, since it has defaults available. Now let's say you put this in
a file called my_first_mesascript.rb
(.rb
is the extension for Ruby files,
by the way). Then to actually generate the inlist, enter
ruby my_first_mesascript.rb
at the command line and watch in awe as
babys_first_inlist
pops into existence. You've created an inlist using
MesaScript, and you did so using fewer lines than it would have taken to
actually make that inlist on your own (technically)!
###Entering Inlist Commands Making blank inlists is boring, so now let's cover how you actually make useful inlists. For mesa inlists, there are really only two types of declarations: those for scalars and those for array. Let's talk about scalars first, since they are far more common. Then we'll get to the more complicated array assignments.
####Scalar Assignments
As an example, let's say we want to set the initial mass of our star to 2.0
solar masses. The inlist command for this is initial_mass
. In a regular
inlist file, we would need to put this in the proper namelist, &controls
as
initial_mass = 2.0
. In MesaScript, there are two ways to do this:
initial_mass 2.0 # this
initial_mass(2.0) # is the same as this
In Ruby, parentheses are optional for method calls, so either way is acceptable. Note that unlike in normal inlists, MesaScript doesn't care about the namelist this attribute belongs to. It'll figure it out on its own and place it appropriately.
WARNING: You cannot use the standard inlist notation of
initial_mass = 2.0 # DON'T EVER DO THIS EVER EVER EVER
it will not throw an error, because it will simply set a new Ruby variable
called initial_mass
. (For the person curious as to why I didn't program this
functionality in, google something like "instance_eval setter method" to
discover what took me too long to figure out.)
####Array Assignments
As an example, let's say we want to set a lower limit on a certain central
abundance as a stopping condition. Then we would, at the minimum, need to set
the inlist command xa_central_lower_limit_species(1) = 'h1'
, for example. In MesaScript, there are three ways to do this:
xa_central_lower_limit_species[1] = 'h1' # These are
xa_central_lower_limit_species(1, 'h1') # all the
xa_central_lower_limit_species 1, 'h1' # same
WARNING: Again, the standard inlist notation for array assignment will not work:
xa_central_lower_limit_species(1) = 'h1' # THIS ENDS IN SADNESS
I tried to program this functionality in, and the kind people at StackOverflow kindly but firmly convinced me it was utterly impossible to to with Ruby without writing a parser of my own. Just stick to the bracket syntax or the less natural parentheses/space notations.
####Other Details That's really all you need to know to start making inlists with MesaScript, though I should remind you, especially if you aren't familiar with Ruby, about the basic types of entries you might use. Most inlist commands are one of the following: booleans, strings, floats, or integers.
Booleans in Ruby are true
and false
(case matters, and no periods).
Strings work the same as in fortran, though
single quotes are more "literal" than double quotes. Double quotes allow for
escaped characters and string interpolation using the #{...}
notation, which
might be useful. For instance,
my_mass = 2.0
initial_mass = my_mass
save_model true
save_model_filename "my_star_#{my_mass}.mod"
will produce (among other things) the line
save_model_filename = 'my_star_2.0.mod'
in the resulting inlist. Note also the
utility of having the initial mass and the save file name being dependent on a
single variable.
Integers are just
integers (I don't know of a useful literal other than just typing out the
entire number, though you can use underscores to make it clearer, e.g.
100_000_000
is the same as 100000000
in Ruby).
Floats use an "e", and never a "d" for an exponential indicator, e.g.
6.02e23
. Ruby floats have arbitrary precision, so there are no doubles.
Finally, if a particular command is giving you trouble, you can always just encase what you want it to be (i.e. in Fortran lingo) in quotes (obviously this does nothing useful if MesaScript is expecting a string). For example
mass_change 1e-7
will have the same effect as
mass_change '1d-7'
since MesaScript will not try to parse '1d-7'
. It was expecting a float, but
since it got a string, it assumes you know better than it.
A useful tidbit is that methods are case sensitive to a point. They have the
same "spelling" as what is found in the .inc
file (like
star/private/star_controls.inc
), but every method has an aliased method that
is the same, but all in lower case, so you don't need to remember the
capitalization so long as you remember the actual spelling.
Any Ruby inside the make_inlist
block will be executed normally, and it can
see variables named outside of the block. So if you have some basic parameters
that can determine a large number of inlist commands, you can simply name those
parameters as variables at the top of your MesaScript file and then make the
actual MesaScript code weave them into your inlist appropriately. This way, the
actual parameter changing from inlist to inlist is taken outside of the actual
inlist commands so you don't forget to change a particular command when you
move on to a different run (like forgetting to change a LOG_dir
, which I've
done a few too many times and thus overwritten some data).
###Deeper and Deeper... Are you still reading this? Well, you must want to do more.
###Using Custom Namelists
You can also make MesaScript know about additional namelists (or forget about
the standard three). After requiring the mesa_script
file, you can change the
namelists it cares about via the following commands (obviously subbing out any
string containing 'namelist1'
or 'namelist2'
with your own appropriate
strings):
require 'mesa_script'
Inlist.namelists = ['namelist1', 'namelist2'] # all namelists you want
# Then indicate the name of the '.inc' files like star/private/star_controls.inc
Inlist.nt_files = {
'namelist1' => 'namelist1_controls.inc',
'namelist2' => 'namelist2_controls.inc'
}
# Then indicate the names of the '.defaults' files like those in star/defaults
Inlist.d_files = {
'namelist1' => 'namelist1.defaults,
'namelist2' => 'namelist2.defaults
}
# Then specify the paths to the files
Inlist.nt_paths ={
'namelist1' => '/path/to/namelist1_controls.inc',
'namelist2' => '/path/to/namelist2_controls.inc'
}
That should set things up to work with custom namelists, so long as the
.inc
and .defaults
files are formatted more or less the same as the "stock"
ones.
###Accessing Current Values and Displaying Default Values Perhaps you want to display a default value in your inlist, but not actually change it. Well, most of the assignment methods mentioned earlier are also getter methods. I haven't mentioned how these methods actually work, so I'll do so now since you're still reading this manifesto.
These methods first flag the name of the data category for going into the
inlist. Then if a new value is supplied to them, it changes the value in the
Inlist
object's internal hash. Then, when all the user-supplied code has been
executed, it gathers all the flagged data and formats it into
properly-formatted namelists, which it then prints out in sequence to the file
name provided by the user. One final note about these methods, they always
return the value associated with the inlist object (the new one if you assign
it, or the current/default value if you don't set one).
So if you want to access any scalar, just call its method without an argument. Not only does this return the default value, but it also flags the category for inclusion in the inlist so
save_this_value = initial_mass
will set save_this_value
to 1.0
(the default value in controls.defaults
)
unless you had already assigned another value, in which case that would be saved
instead. Additionally, initial_mass = 1.0
will appear in the final inlist,
even though we didn't give initial_mass
a new value. In fact, we could just
have a line like
initial_z
that neither uses the return value nor changes the stored value. This will just
flag initial_z
for being put in the final inlist. Note that there is
currently no way to unflag an inlist item.
For arrays, things work like you might expect. Any time any one of the versions of the array methods are called, that entire array category is staged for inclusion in the inlist. For example, you could do any of the following:
xa_central_lower_limit # returns a hash of values
xa_central_lower_limit[1] # returns the value associated with 1 in the hash
xa_central_lower_limit(1) # same as above
xa_central_lower_limit 1 # same as above
Note that these array methods, as indicated, point to hashes (not arrays) of
values. So xa_central_lower_limit_species[1] = 'h1'
would return
{1 => 'h1'}
.
##Further Work I warmly welcome bug reports, feature suggestions, and most all, pull requests!