mesa_script 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +292 -0
  3. data/bin/inlist2mesascript +24 -0
  4. data/lib/mesa_script.rb +618 -0
  5. metadata +54 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ca9b7036396472f5a9ea98119ef282a875bac4e8
4
+ data.tar.gz: 6e2c6f0ffa3ef59365fd03815ea81b88fa5b316c
5
+ SHA512:
6
+ metadata.gz: 46b15cee048454f84c79bfc6deaafe9356b91cfd60959599b7d874ab3a80194118d88e19c91b0ce26e9084933c65f52f8f37624b0f56a2c40b9fab78c75c5f0e
7
+ data.tar.gz: 28b1bc30eec9a956f760b30fda344c54f869f266fc94cc305c5893ce704efe9d13c629ba32256730dcadd799966faff035250b3f0547880604d7bfc92a3c772f
data/README.md ADDED
@@ -0,0 +1,292 @@
1
+ MesaScript
2
+ ==========
3
+
4
+ ###MESA Requirement!
5
+ In its current state, MesaScript requires MESA rev. 5596 or above. This is due
6
+ to a sensitivity to where the `'.inc'` files are stored on earlier versions. If
7
+ there is demand for MesaScript for earlier revisions, I will look into making
8
+ it backward compatible.
9
+
10
+ ###The Short Short Version
11
+ To get up and running fast, skip to installation, then try and use the included
12
+ sample file, `sample.rb` (via running `ruby sample.rb` in the command line). The
13
+ comments in `sample.rb` should get you started, especially if you have at least
14
+ a little Ruby know-how.
15
+ ###What is MesaScript?
16
+ Lightweight, Ruby-based language that provides a powerful way to make inlists
17
+ for MESA projects. Really, MesaScript is a DSL (domain-specific language) built
18
+ on top of Ruby, so Ruby code "just works" inside MesaScript (if you're familiar
19
+ with it, think of what SASS is to CSS, but on a smaller scale).
20
+
21
+ ### What does MesaScript do?
22
+ MesaScript provides a way to build inlists for use with
23
+ [MESA](http://mesa.sourceforge.net) using Ruby, though you need not know much
24
+ at all about Ruby to use it. The main point is that you can use
25
+ variables when creating an inlist, making a reusable template for parameter
26
+ space studies when only a few inlist commands vary between a large number of
27
+ inlists. Most, if not all, of what MesaScript does can be done by using MESA's
28
+ `run_star_extras` hooks, but for the purposes of documenting what I do with
29
+ MESA, I find inlists more enlightening, and I try to stick to high-level
30
+ languages whenever I can.
31
+
32
+ There are other benefits, too. MesaScript automatically checks your input to
33
+ make sure that the types of arguments you give for various namelist items match
34
+ what is expected, and the resulting inlist is neatly formatted and sensibly
35
+ ordered. You can also easily convert an existing inlist to MesaScript for
36
+ editing and further generalization. In general, *writing an inlist in*
37
+ *MesaScript is no more difficult than writing a normal inlist, but you have far*
38
+ *more flexibility*. So why not give it a try?
39
+
40
+ If you know a little Ruby (want to learn?
41
+ [Try Ruby here!](http://tryruby.org/levels/1/challenges/0)), the possibilities
42
+ are pretty wide open. You could easily make a script that starts with a given
43
+ set of parameters, run MESA star, then use the output of that run to dictate a
44
+ new inlist and run, creating a chain (maybe a MESA root find of sorts).
45
+
46
+ ###Installation
47
+ Someday, I hope to package this as a gem, but for now, it's staying hosted on
48
+ Github, which means you need to install it yourself. Clone or otherwise
49
+ download the repository somewhere to your home directory with
50
+
51
+ git clone https://github.com/wmwolf/MesaScript.git ~/MesaScript
52
+
53
+ or somewhere else to your liking.
54
+
55
+ Then, either copy the file `mesa_script.rb` to somewhere along Ruby's path, or
56
+ set up another stand-in file that points to your `mesa_script.rb` file in
57
+ Ruby's path. To find Ruby's path, type
58
+
59
+ ruby -e 'puts $:'
60
+
61
+ in your terminal. If Ruby is properly configured, as it is on most modern Unix
62
+ systems, you should see a list of possible directories. Either copy
63
+ `mesa_script.rb` there or do what I do and make a new file called
64
+ `mesa_script.rb` there and have it just be
65
+
66
+ require '/PATH/TO/YOUR/CLONED/REPOSITORY/mesa_script.rb'
67
+
68
+ This way, if you later update your repo via `git pull`, you won't need to copy
69
+ `mesa_script.rb` again. Also, if you'd like to use the included (optional)
70
+ `inlist2mesascript` tool, copy that to somewhere along you system's path (
71
+ `echo $PATH`). Then type `inlist2mesascript -h` to learn more about that tool.
72
+ As you
73
+ might guess, it takes an existing MESA inlist and converts it to a file in
74
+ MesaScript that, if executed by Ruby should produce essentially the same inlist
75
+ (good for moving a project to MesaScript).
76
+
77
+ To check if Ruby can see the file, try doing `ruby -e 'require "mesa_script"'`.
78
+ If no error occurs, it is working fine.
79
+
80
+ Finally, you must have your `MESA_DIR` environment variable set for anything to
81
+ work. The `mesa_script.rb` file generates all the necessary data it needs from
82
+ the MESA source on the fly (this also makes it nearly MESA version
83
+ independent).
84
+
85
+ ###Basic Usage
86
+ The `mesa_script.rb` file defines just one class, Inlist, which we'll interact
87
+ with primarily through one class method, `make_inlist`. Just put the following
88
+ in a file to make a blank inlist:
89
+
90
+ require 'mesa_script'
91
+
92
+ Inlist.make_inlist('babys_first_inlist') {
93
+ # inlist commands go here
94
+ }
95
+
96
+ This creates a file called `babys_first_inlist` that will be pretty
97
+ boring. It will create three namelists (the usual `star_job`, `controls`, and
98
+ `pgstar`) and leaves them blank inside, which is a perfectly acceptable inlist
99
+ for MESA to use, since it has defaults available. Now let's say you put this in
100
+ a file called `my_first_mesascript.rb` (`.rb` is the extension for Ruby files,
101
+ by the way). Then to actually generate the inlist, enter
102
+ `ruby my_first_mesascript.rb` at the command line and watch in awe as
103
+ `babys_first_inlist` pops into existence. You've created an inlist using
104
+ MesaScript, and you did so using fewer lines than it would have taken to
105
+ actually make that inlist on your own (technically)!
106
+
107
+ ###Entering Inlist Commands
108
+ Making blank inlists is boring, so now let's cover how you actually make useful
109
+ inlists. For mesa inlists, there are really only two types of declarations:
110
+ those for scalars and those for array. Let's talk about scalars first, since
111
+ they are far more common. Then we'll get to the more complicated array
112
+ assignments.
113
+
114
+ ####Scalar Assignments
115
+ As an example, let's say we want to set the initial mass of our star to 2.0
116
+ solar masses. The inlist command for this is `initial_mass`. In a regular
117
+ inlist file, we would need to put this in the proper namelist, `&controls` as
118
+ `initial_mass = 2.0`. In MesaScript, there are two ways to do this:
119
+
120
+ initial_mass 2.0 # this
121
+ initial_mass(2.0) # is the same as this
122
+
123
+ In Ruby, parentheses are optional for method calls, so either way is
124
+ acceptable. Note that unlike in normal inlists, MesaScript doesn't care about
125
+ the namelist this attribute belongs to. It'll figure it out on its own and
126
+ place it appropriately.
127
+
128
+ **WARNING**: You *cannot* use the standard inlist notation of
129
+
130
+ initial_mass = 2.0 # DON'T EVER DO THIS EVER EVER EVER
131
+
132
+ it will *not* throw an error, because it will simply set a new Ruby variable
133
+ called `initial_mass`. (For the person curious as to why I didn't program this
134
+ functionality in, google something like "instance_eval setter method" to
135
+ discover what took me too long to figure out.)
136
+
137
+ ####Array Assignments
138
+ As an example, let's say we want to set a lower limit on a certain central
139
+ abundance as a stopping condition. Then we would, at the minimum, need to set
140
+ the inlist command `xa_central_lower_limit_species(1) = 'h1'`, for example. In MesaScript, there are three ways to do this:
141
+
142
+ xa_central_lower_limit_species[1] = 'h1' # These are
143
+ xa_central_lower_limit_species(1, 'h1') # all the
144
+ xa_central_lower_limit_species 1, 'h1' # same
145
+
146
+ **WARNING**: Again, the standard inlist notation for array assignment will not
147
+ work:
148
+
149
+ xa_central_lower_limit_species(1) = 'h1' # THIS ENDS IN SADNESS
150
+
151
+ I tried to program this functionality in, and the kind people at
152
+ [StackOverflow](http://stackoverflow.com/questions/21036873/how-do-i-write-a-method-to-edit-an-array-hash-using-parentheses-instead-of-squar/21044781?noredirect=1#21044781) 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.
153
+
154
+ ####Other Details
155
+ That's really all you need to know to start making inlists with MesaScript,
156
+ though I should remind you, especially if you aren't familiar with Ruby, about
157
+ the basic types of entries you might use. Most inlist commands are one of the
158
+ following: booleans, strings, floats, or integers.
159
+
160
+ **Booleans** in Ruby are `true` and `false` (case matters, and no periods).
161
+
162
+ **Strings** work the same as in fortran, though
163
+ single quotes are more "literal" than double quotes. Double quotes allow for
164
+ escaped characters and string interpolation using the `#{...}` notation, which
165
+ might be useful. For instance,
166
+
167
+ my_mass = 2.0
168
+ initial_mass = my_mass
169
+ save_model true
170
+ save_model_filename "my_star_#{my_mass}.mod"
171
+
172
+ will produce (among other things) the line
173
+ `save_model_filename = 'my_star_2.0.mod'` in the resulting inlist. Note also the
174
+ utility of having the initial mass and the save file name being dependent on a
175
+ single variable.
176
+
177
+ **Integers** are just
178
+ integers (I don't know of a useful literal other than just typing out the
179
+ entire number, though you can use underscores to make it clearer, e.g.
180
+ `100_000_000` is the same as `100000000` in Ruby).
181
+
182
+ **Floats** use an "e", and never a "d" for an exponential indicator, e.g.
183
+ `6.02e23`. Ruby floats have arbitrary precision, so there are no doubles.
184
+
185
+ 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
186
+
187
+ mass_change 1e-7
188
+
189
+ will have the same effect as
190
+
191
+ mass_change '1d-7'
192
+
193
+ since MesaScript will not try to parse `'1d-7'`. It was expecting a float, but
194
+ since it got a string, it assumes you know better than it.
195
+
196
+ A useful tidbit is that methods are case sensitive to a point. They have the
197
+ same "spelling" as what is found in the `.inc` file (like
198
+ `star/private/star_controls.inc`), but every method has an aliased method that
199
+ is the same, but all in lower case, so you don't need to remember the
200
+ capitalization so long as you remember the actual spelling.
201
+
202
+ Any Ruby inside the `make_inlist` block will be executed normally, and it can
203
+ see variables named outside of the block. So if you have some basic parameters
204
+ that can determine a large number of inlist commands, you can simply name those
205
+ parameters as variables at the top of your MesaScript file and then make the
206
+ actual MesaScript code weave them into your inlist appropriately. This way, the
207
+ actual parameter changing from inlist to inlist is taken outside of the actual
208
+ inlist commands so you don't forget to change a particular command when you
209
+ move on to a different run (like forgetting to change a `LOG_dir`, which I've
210
+ done a few too many times and thus overwritten some data).
211
+
212
+ ###Deeper and Deeper...
213
+ Are you still reading this? Well, you must want to do more.
214
+
215
+ ###Using Custom Namelists
216
+ You can also make MesaScript know about additional namelists (or forget about
217
+ the standard three). After requiring the `mesa_script` file, you can change the
218
+ namelists it cares about via the following commands (obviously subbing out any
219
+ string containing `'namelist1'` or `'namelist2'` with your own appropriate
220
+ strings):
221
+
222
+ require 'mesa_script'
223
+
224
+ Inlist.namelists = ['namelist1', 'namelist2'] # all namelists you want
225
+
226
+ # Then indicate the name of the '.inc' files like star/private/star_controls.inc
227
+ Inlist.nt_files = {
228
+ 'namelist1' => 'namelist1_controls.inc',
229
+ 'namelist2' => 'namelist2_controls.inc'
230
+ }
231
+ # Then indicate the names of the '.defaults' files like those in star/defaults
232
+ Inlist.d_files = {
233
+ 'namelist1' => 'namelist1.defaults,
234
+ 'namelist2' => 'namelist2.defaults
235
+ }
236
+ # Then specify the paths to the files
237
+ Inlist.nt_paths ={
238
+ 'namelist1' => '/path/to/namelist1_controls.inc',
239
+ 'namelist2' => '/path/to/namelist2_controls.inc'
240
+ }
241
+
242
+ That *should* set things up to work with custom namelists, so long as the
243
+ `.inc` and `.defaults` files are formatted more or less the same as the "stock"
244
+ ones.
245
+
246
+ ###Accessing Current Values and Displaying Default Values
247
+ Perhaps you want to display a default value in your inlist, but not actually
248
+ change it. Well, most of the assignment methods mentioned earlier
249
+ 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.
250
+
251
+ These methods first flag the name of the data category for going into the
252
+ inlist. Then if a new value is supplied to them, it changes the value in the
253
+ `Inlist` object's internal hash. Then, when all the user-supplied code has been
254
+ executed, it gathers all the flagged data and formats it into
255
+ properly-formatted namelists, which it then prints out in sequence to the file
256
+ name provided by the user. One final note about these methods, they always
257
+ return the value associated with the inlist object (the new one if you assign
258
+ it, or the current/default value if you don't set one).
259
+
260
+ So if you want to access any scalar, just call its method without an argument.
261
+ Not only does this return the default value, but it also flags the category for
262
+ inclusion in the inlist so
263
+
264
+ save_this_value = initial_mass
265
+
266
+ will set `save_this_value` to `1.0` (the default value in `controls.defaults`)
267
+ unless you had already assigned another value, in which case that would be saved
268
+ instead. Additionally, `initial_mass = 1.0` will appear in the final inlist,
269
+ even though we didn't give `initial_mass` a new value. In fact, we could just
270
+ have a line like
271
+
272
+ initial_z
273
+
274
+ that neither uses the return value nor changes the stored value. This will just
275
+ flag `initial_z` for being put in the final inlist. Note that there is
276
+ currently no way to unflag an inlist item.
277
+
278
+ For arrays, things work like you might expect. Any time any one of the versions
279
+ of the array methods are called, that entire array category is staged for
280
+ inclusion in the inlist. For example, you could do any of the following:
281
+
282
+ xa_central_lower_limit # returns a hash of values
283
+ xa_central_lower_limit[1] # returns the value associated with 1 in the hash
284
+ xa_central_lower_limit(1) # same as above
285
+ xa_central_lower_limit 1 # same as above
286
+
287
+ Note that these array methods, as indicated, point to hashes (not arrays) of
288
+ values. So `xa_central_lower_limit_species[1] = 'h1'` would return
289
+ `{1 => 'h1'}`.
290
+
291
+ ##Further Work
292
+ I warmly welcome bug reports, feature suggestions, and most all, pull requests!
@@ -0,0 +1,24 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'mesa_script'
4
+
5
+ if ARGV.size == 0 or %w[-h --help].include?(ARGV[0])
6
+ puts ''
7
+ puts "inlist2mesascript help:"
8
+ puts '-----------------------'
9
+ puts "Only one command, which converts an inlist to a mesascript file:"
10
+ puts ''
11
+ puts 'inlist2mesascript SOURCE OUTPUT.rb'
12
+ puts ''
13
+ puts 'where SOURCE is your mesa inlist and OUTPUT is the name of the'
14
+ puts "mesascript file to be produced. '.rb' will not be appended, though"
15
+ puts "the resulting file will be a ruby/mesascript file."
16
+ puts ''
17
+ elsif ARGV.size == 2
18
+ source = ARGV[0]
19
+ output = ARGV[1]
20
+
21
+ Inlist.inlist_to_mesascript(source, output, false)
22
+ else
23
+ raise "Expected two arguments (received #{ARGV.size}). Enter 'inlist2mesa -h' for help."
24
+ end
@@ -0,0 +1,618 @@
1
+ InlistItem = Struct.new(:name, :type, :value, :namelist, :order, :is_arr,
2
+ :num_indices, :flagged)
3
+
4
+ class Inlist
5
+
6
+
7
+ @inlist_data = {}
8
+ # Different namelists can be added or subtracted if MESA should change or
9
+ # proprietary inlists are required. Later hashes should be edited in a
10
+ # similar way to get the desired behavior for additional namelists.
11
+
12
+ #################### ADD NEW NAMELISTS HERE ####################
13
+ @namelists = %w{ star_job controls pgstar }
14
+ ################## POINT TO .INC FILES HERE ####################
15
+ @nt_files = {
16
+ 'star_job' => %w{star_job_controls.inc},
17
+ 'controls' => %w{star_controls.inc ctrls_io.f},
18
+ 'pgstar' => %w{pgstar_controls.inc}
19
+ }
20
+ # User can specify a custom name for a namelist defaults file. The default
21
+ # is simply the namelist name followed by '.defaults'
22
+
23
+ ################ POINT TO .DEFAULTS FILES HERE #################
24
+ @d_files = {}
25
+
26
+ # User can add new paths to namelist default files through this hash
27
+
28
+ ############ GIVE PATHS TO .INC AND .DEF FILES HERE ###########
29
+ @nt_paths = Hash.new(ENV['MESA_DIR'] + '/star/private/')
30
+ @d_paths = Hash.new(ENV['MESA_DIR'] + '/star/defaults/')
31
+
32
+
33
+ ############### NO MORE [SIMPLE] USER-CUSTOMIZABLE FEATURES BELOW ##############
34
+
35
+ # This tells the class to initialize its structure if it hasn't already.
36
+ # If new namelists are added after an instance is initialized, this can be
37
+ # redone manually by the Inlist.get_data command.
38
+ @have_data = false
39
+
40
+ # Set up interface to access/change customizable inlist initialization data.
41
+ # Establish class instance variables
42
+ class << self
43
+ attr_accessor :have_data
44
+ attr_accessor :namelists, :nt_paths, :d_paths, :inlist_data, :d_files,
45
+ :nt_files
46
+ end
47
+
48
+ # Generate methods for the Inlist class that set various namelist parameters.
49
+ def self.get_data
50
+ Inlist.namelists.each do |namelist|
51
+ @inlist_data[namelist] = Inlist.get_namelist_data(namelist,
52
+ Inlist.nt_files[namelist], Inlist.d_files[namelist])
53
+ end
54
+ # create methods (interface) for each data category
55
+ @inlist_data.each_value do |namelist_data|
56
+ namelist_data.each do |datum|
57
+ if datum.is_arr
58
+ Inlist.make_parentheses_method(datum)
59
+ else
60
+ Inlist.make_regular_method(datum)
61
+ end
62
+ end
63
+ end
64
+ # don't do this nonsense again unles specifically told to do so
65
+ Inlist.have_data = true
66
+ end
67
+
68
+ # Three ways to access array categories. All methods will cause the
69
+ # data category to be staged into your inlist, even if you do not change it
70
+ # Basically, if it appears in your mesascript, it will definitely appear
71
+ # in your inlist. There is no way to unflag an entry.
72
+ #
73
+ # 1. Standard array way like
74
+ # xa_lower_limit_species[1] = 'h1'
75
+ # (note square braces, NOT parentheses). Returns new value.
76
+ #
77
+ # 2. Just access (and flag), but don't change via array access, like
78
+ # xa_lower_limit_species[1]
79
+ # (again, note square braces). Returns current value
80
+ #
81
+ # 3. No braces method, like
82
+ # xa_lower_limit_species() # flags and returns hash of values
83
+ # xa_lower_limit_species # same, but more ruby-esque
84
+ # xa_lower_limit_species(1) # flags and returns value 1
85
+ # xa_lower_limit_species 1 # Same
86
+ # xa_lower_limit_species(1, 'h1') # flags and sets value 1
87
+ # xa_lower_limit_species 1, 'h1' # same
88
+ #
89
+ # For multi-dimensional arrays, things are even more vaired. You can treat
90
+ # them like 1-dimensional arrays with the "index" just being an array of
91
+ # indices, for instance:
92
+ #
93
+ # text_summary1_name[[1,2]] = 'star_mass' # flags ALL values and sets
94
+ # text_summary1_name([1,2], 'star_mass') # text_summary1_name(1,2)
95
+ # text_summary1_name [1,2], 'star_mass # to 'star_mass'
96
+ #
97
+ # text_summary1_name [1,2] # flags ALL values and
98
+ # text_summary1_name([1,2]) # returns
99
+ # # text_sumarry_name(1,2)
100
+ #
101
+ # text_summary_name() # flags ALL values and
102
+ # text_summary_name # returns entire hash for
103
+ # # text_summary_name
104
+ #
105
+ # Alternatively, can use the more intuitive form where indices are separate
106
+ # and don't need to be in an array, but this only works with the parentheses
107
+ # versions (i.e. the first option directly above has no counterpart):
108
+ #
109
+ # text_summary1_name(1, 2, 'star_mass')
110
+ # text_summary1_name 1, 2, 'star_mass' # same as above (first 3)
111
+ #
112
+ # text_summary1_name
113
+
114
+ def self.make_parentheses_method(datum)
115
+ name = datum.name
116
+ num_indices = datum.num_indices
117
+ define_method(name + '[]=') do|arg1, arg2|
118
+ if num_indices > 1
119
+ raise "First argument of #{name}[]= (part in brackets) must be an array with #{num_indices} indices since #{name} is a multi-dimensional array." unless (arg1.is_a?(Array) and arg1.length == num_indices)
120
+ end
121
+ self.flag_command(name)
122
+ self.data_hash[name].value[arg1] = arg2
123
+ end
124
+ define_method(name + '[]') do |arg|
125
+ if num_indices > 1
126
+ raise "Argument of #{name}[] (part in brackets) must be an array with #{num_indices} indices since #{name} is a multi-dimensional array." unless (arg.is_a?(Array) and arg.length == num_indices)
127
+ end
128
+ self.flag_command(name)
129
+ self.data_hash[name].value[arg]
130
+ end
131
+ define_method(name) do |*args|
132
+ self.flag_command(name)
133
+ case args.length
134
+ when 0 then self.data_hash[name].value
135
+ when 1
136
+ if num_indices > 1
137
+ raise "First argument of #{name} must be an array with #{num_indices} indices since #{name} is a multi-dimensional array OR must provide all indices as separate arguments." unless (args[0].is_a?(Array) and args[0].length == num_indices)
138
+ end
139
+ self.data_hash[name].value[args[0]]
140
+ when 2
141
+ if num_indices == 1 and (not args[0].is_a?(Array))
142
+ self.data_hash[name].value[args[0]] = args[1]
143
+ elsif num_indices == 2 and (not args[0].is_a?(Array)) and args[1].is_a?(Fixnum)
144
+ self.data_hash[name].value[args]
145
+ elsif num_indices > 1
146
+ raise "First argument of #{name} must be an array with #{num_indices} indices since #{name} is a multi-dimensional array OR must provide all indices as separate arguments." unless (args[0].is_a?(Array) and args[0].length == num_indices)
147
+ self.data_hash[name].value[args[0]] = args[1]
148
+ else
149
+ raise "First argument of #{name} must be an array with #{num_indices} indices since #{name} is a multi-dimensional array OR must provide all indices as separate arguments. The optional final argument is what the #{name} would be set to. Omission of this argument will simply flag #{name} to appear in the inlist."
150
+ end
151
+ when num_indices
152
+ self.data_hash[name].value[args]
153
+ when num_indices + 1
154
+ raise "Bad arguments for #{name}. Either provide an array of #{num_indices} indices for the first argument or provide each index in succession, optionally specifying the desired value for the last argument." if args[0].is_a?(Array)
155
+ self.data_hash[name].value[args[0..-2]] = args[-1]
156
+ else
157
+ raise "Wrong number of arguments for #{name}. Can provide zero arguments (just flag command), one argument (array of indices for multi-d array or one index for 1-d array), two arguments (array of indices/single index for multi-/1-d array and a new value for the value), #{num_indices} arguments where the elements themselves are the right indices (returns the specified element of the array), or #{num_indices + 1} arguments to set the specific value and return it."
158
+ end
159
+ end
160
+ alias_method name.downcase.to_sym, name.to_sym
161
+ alias_method (name.downcase + '[]').to_sym, (name + '[]').to_sym
162
+ alias_method (name.downcase + '[]=').to_sym, (name + '[]=').to_sym
163
+ end
164
+
165
+ # Two ways to access/change scalars. All methods will cause the data category
166
+ # to be staged into your inlist, even if you do not change the value.
167
+ # Basically, if it appears in your mesascript, it will definitely appear in
168
+ # your inlist. There is no way to unflag an entry.
169
+ #
170
+ # 1. Change value, like
171
+ # initial_mass(1.0)
172
+ # initial_mass 1.0
173
+ # This flags the category to go in your inlist and changes the value. There
174
+ # is no difference between these two syntaxes (it's built into ruby).
175
+ # Returns new value.
176
+ #
177
+ # 2. Just access, like
178
+ # initial_mass()
179
+ # initial_mass
180
+ # This flags the category, but does not change the value. Again, both
181
+ # syntaxes are allowed, though the one without parentheses is more
182
+ # traditional for ruby (why do you want empty parentheses anyway?). Returns
183
+ # current value.
184
+
185
+ def self.make_regular_method(datum)
186
+ name = datum.name
187
+ define_method(name) do |*args|
188
+ self.flag_command(name)
189
+ return self.data_hash[name].value if args.empty?
190
+ self.data_hash[name].value = args[0]
191
+ end
192
+ aliases = [(name + '=').to_sym,
193
+ (name.downcase + '=').to_sym,
194
+ name.downcase.to_sym]
195
+ aliases.each { |ali| alias_method ali, name.to_sym }
196
+ end
197
+
198
+
199
+ # Ensure provided value's data type matches expected data type. Then convert
200
+ # to string for printing to an inlist. If value is a string, change nothing
201
+ # (no protection). If value is a string and SHOULD be a string, wrap it in
202
+ # single quotes.
203
+ def self.parse_input(name, value, type)
204
+ if value.class == String
205
+ if type == :string
206
+ value = "'#{value}'" unless value[0] == "'" and value[-1] == "'"
207
+ end
208
+ return value
209
+ elsif type == :bool
210
+ unless [TrueClass, FalseClass].include?(value.class)
211
+ raise "Invalid value for namelist item #{name}: #{value}. Use " +
212
+ "'.true.', '.false.', or a Ruby boolean (true/false)."
213
+ end
214
+ if value == true
215
+ return '.true.'
216
+ elsif value == false
217
+ return '.false.'
218
+ else
219
+ raise "Error converting value #{value} of #{name} to a boolean."
220
+ end
221
+ elsif type == :int
222
+ raise "Invalid value for namelist item #{name}: #{value}. Must provide"+
223
+ " an int or float." unless value.is_a?(Integer) or value.is_a?(Float)
224
+ if value.is_a?(Float)
225
+ puts "WARNING: Expected integer for #{name} but got #{value}. Value" +
226
+ " will be converted to an integer."
227
+ end
228
+ return value.to_i.to_s
229
+ elsif type == :float
230
+ raise "Invalid value for namelist item #{name}: #{value}. Must provide " +
231
+ "an int or float." unless value.is_a?(Integer) or value.is_a?(Float)
232
+ return sprintf("%g", value).sub('e', 'd')
233
+ elsif type == :type
234
+ puts "WARNING: 'type' values are currently unsupported (regarding #{name}) because your humble author has no idea what they look like in an inlist. You should tell him what to do at wmwolf@physics.ucsb.edu. Your input, #{value}, has been passed through to your inlist verbatim."
235
+ return value.to_s
236
+ else
237
+ raise "Error parsing value for namelist item #{name}: #{value}. Expected "
238
+ "type was #{type}."
239
+ end
240
+ end
241
+
242
+ # Converts a standard inlist to its equivalent mesascript formulation.
243
+ # Comments are preserved and namelist separators are converted to comments.
244
+ # Note that comments do NOT get put back into the fortran inlist through
245
+ # mesascript. Converting an inlist to mesascript and then back again will
246
+ # clean up and re-order your inlist, but all comments will be lost. All other
247
+ # information SHOULD remain intact.
248
+ def self.inlist_to_mesascript(inlist_file, script_file, dbg = false)
249
+ Inlist.get_data unless Inlist.have_data # ensure we have inlist data
250
+ inlist_contents = File.readlines(inlist_file)
251
+
252
+ # make namelist separators comments
253
+ new_contents = inlist_contents.map do |line|
254
+ case line
255
+ when /^\s*&/ then '# ' + line.chomp # start namelist
256
+ when /^\s*\// then '# ' + line.chomp # end namelist
257
+ else
258
+ line.sub('!', '#').chomp # fix comments
259
+ end
260
+ end
261
+ new_contents.map! do |line|
262
+ if line =~ /^\s*#/ or line.strip.empty? # leave comments and blanks
263
+ result = line
264
+ else
265
+ if dbg
266
+ puts "parsing line:"
267
+ puts line
268
+ end
269
+ comment_pivot = line.index('#')
270
+ if comment_pivot
271
+ command = line[0...comment_pivot]
272
+ comment = line[comment_pivot..-1].to_s.strip
273
+ else
274
+ command = line
275
+ comment = ''
276
+ end
277
+ command =~ /(^\s*)/ # save leading space
278
+ leading_space = Regexp.last_match(1)
279
+ command =~ /(\s*$)/ # save buffer space
280
+ buffer_space = Regexp.last_match(1)
281
+ command.strip! # remove white space
282
+ name, value = command.split('=').map { |datum| datum.strip }
283
+ if dbg
284
+ puts "name: #{name}"
285
+ puts "value: #{value}"
286
+ end
287
+ if name =~ /\((\d+)\)/ # fix 1D array assignments
288
+ name.sub!('(', '[')
289
+ name.sub!(')', ']')
290
+ name = name + ' ='
291
+ elsif name =~ /\((\s*\d+\s*,\s*)+\d\s*\)/ # fix multi-D arrays
292
+ # arrays become hashes in MesaScript, so rather than having multiple
293
+ # indices, the key becomes the array of indices themselves, hence
294
+ # the double braces replacing single parentheses
295
+ name.sub!('(', '[[')
296
+ name.sub!(')', ']]')
297
+ name = name + ' ='
298
+ end
299
+ name.downcase!
300
+ if value =~ /'.*'/ or value =~ /".*"/
301
+ result = name + ' ' + value # leave strings alone
302
+ elsif %w[.true. .false.].include?(value.downcase)
303
+ result = name + ' ' + value.downcase.gsub('.', '') # fix booleans
304
+ elsif value =~ /\d+\.?\d*([eEdD]\d+)?/
305
+ result = name + ' ' + value.downcase.sub('d', 'e') # fix floats
306
+ else
307
+ result = name + ' ' + value # leave everything else alone
308
+ end
309
+ result = leading_space + result + buffer_space + comment
310
+ if dbg
311
+ puts "parsed to:"
312
+ puts result
313
+ puts ''
314
+ end
315
+ end
316
+ result
317
+ end
318
+ File.open(script_file, 'w') do |f|
319
+ f.puts "require 'mesa_script'"
320
+ f.puts ''
321
+ f.puts "Inlist.make_inlist('#{File.basename(inlist_file)}') do"
322
+ new_contents.each { |line| f.puts ' ' + line }
323
+ f.puts "end"
324
+ end
325
+ end
326
+
327
+
328
+ # Create an Inlist object, execute block of commands that presumably populate
329
+ # the inlist, then write the inlist to a file with the given name. This is
330
+ # the money routine with user-supplied commands in the instance_eval block.
331
+ def self.make_inlist(name = 'inlist', &block)
332
+ inlist = Inlist.new
333
+ inlist.instance_eval(&block)
334
+ inlist.stage_flagged
335
+ File.open(name, 'w') { |f| f.write(inlist) }
336
+ end
337
+
338
+ # Checks to see if the data/methods for the Inlist class has been initialized.
339
+ def self.have_data?
340
+ @have_data
341
+ end
342
+
343
+ # Reads names and types for a specified namelist from given file (intended
344
+ # to be of the form of something like star/private/star_controls.inc).
345
+ #
346
+ # Returns an array of InlistItem Struct instances that contain a parameter's
347
+ # name, type (:bool, :string, :float, :int, or :type), the namelist it
348
+ # belongs to, and its relative ordering in that namelist. Bogus defaults are
349
+ # assigned according to the object's type, and the ordering is unknown.
350
+
351
+ def self.get_namelist_data(namelist, nt_filename = nil, d_filename = nil)
352
+ temp_data = Inlist.get_names_and_types(namelist, nt_filename)
353
+ Inlist.get_defaults(temp_data, namelist, d_filename)
354
+ end
355
+
356
+ def self.get_names_and_types(namelist, nt_filenames = nil)
357
+ nt_filenames ||= Inlist.nt_files[namelist]
358
+ unless nt_filenames.respond_to?(:each)
359
+ nt_filenames = [nt_filenames]
360
+ end
361
+ nt_full_paths = nt_filenames.map { |file| Inlist.nt_paths[namelist] + file }
362
+
363
+ namelist_data = []
364
+
365
+ nt_full_paths.each do |nt_full_path|
366
+ unless File.exists?(nt_full_path)
367
+ raise "Couldn't find file #{nt_full_path}"
368
+ end
369
+ contents = File.readlines(nt_full_path)
370
+
371
+ # Throw out comments and blank lines, ensure remaining lines are a proper
372
+ # Fortran assignment, then remove leading and trailing white space
373
+ contents.reject! { |line| is_comment?(line) or is_blank?(line) }
374
+ contents.map! do |line|
375
+ my_line = line.dup
376
+ my_line = my_line[0...my_line.index('!')] if has_comment?(my_line)
377
+ my_line.strip!
378
+ end
379
+ full_lines = []
380
+ contents.each_with_index do |line, i|
381
+ break if line =~ /\A\s*contains/
382
+ next unless line =~ /::/
383
+ full_lines << Inlist.full_line(contents, i)
384
+ end
385
+ pairs = full_lines.map do |line|
386
+ line.split('::').map { |datum| datum.strip}
387
+ end
388
+ pairs.each do |pair|
389
+ type = case pair[0]
390
+ when /logical/ then :bool
391
+ when /character/ then :string
392
+ when /real/ then :float
393
+ when /integer/ then :int
394
+ when /type/ then :type
395
+ else
396
+ raise "Couldn't determine type of entry #{pair[0]} in " +
397
+ "#{nt_full_path}."
398
+ end
399
+ name_chars = pair[1].split('')
400
+ names = []
401
+ paren_level = 0
402
+ name_chars.each do |char|
403
+ if paren_level > 0 and char == ','
404
+ names << '!'
405
+ next
406
+ elsif char == '('
407
+ paren_level += 1
408
+ elsif char == ')'
409
+ paren_level -= 1
410
+ end
411
+ names << char
412
+ end
413
+ names = names.join.split(',').map { |name| name.strip }
414
+ names.each do |name|
415
+ is_arr = false
416
+ num_indices = 0
417
+ if name =~ /\(.*\)/
418
+ is_arr = true
419
+ num_indices = name.count('!') + 1
420
+ name.sub!(/\(.*\)/, '')
421
+ end
422
+ type_default = {:bool => false, :string => '', :float => 0.0,
423
+ :int => 0}
424
+ dft = is_arr ? Hash.new(type_default[type]) : type_default[type]
425
+ namelist_data << InlistItem.new(name, type, dft, namelist, -1, is_arr,
426
+ num_indices)
427
+ end
428
+ end
429
+ end
430
+ namelist_data
431
+ end
432
+
433
+ # Similar to Inlist.get_names_and_types, but takes the output of
434
+ # Inlist.get_names_and_types and assigns defaults and orders to each item.
435
+ # Looks for this information in the specified defaults filename.
436
+
437
+ def self.get_defaults(temp_data, namelist, d_filename = nil, whine = false)
438
+ d_filename ||= namelist + '.defaults'
439
+ d_full_path = Inlist.d_paths[namelist] + d_filename
440
+ raise "Couldn't find file #{d_filename}" unless File.exists?(d_full_path)
441
+ contents = File.readlines(d_full_path)
442
+ contents.reject! { |line| is_comment?(line) or is_blank?(line) }
443
+ contents.map! do |line|
444
+ my_line = line.dup
445
+ if has_comment?(line)
446
+ my_line = my_line[0...my_line.index('!')]
447
+ end
448
+ raise "Equal sign missing in line:\n\t #{my_line}\n in file " +
449
+ "#{full_path}." unless my_line =~ /=/
450
+ my_line.strip!
451
+ end
452
+ pairs = contents.map {|line| line.split('=').map {|datum| datum.strip}}
453
+ n_d_hash = {}
454
+ n_o_hash = {}
455
+ pairs.each_with_index do |pair, i|
456
+ name = pair[0]
457
+ default = pair[1]
458
+ if name =~ /\(.*\)/
459
+ selector = name[/\(.*\)/][1..-2]
460
+ name.sub!(/\(.*\)/, '')
461
+ if selector.include?(':')
462
+ default = Hash.new(default)
463
+ elsif selector.count(',') == 0
464
+ default = {selector.to_i => default}
465
+ else
466
+ selector = selector.split(',').map { |index| index.strip.to_i }
467
+ default = default = {selector => default}
468
+ end
469
+ end
470
+ if n_d_hash[name].is_a?(Hash)
471
+ n_d_hash[name].merge!(default)
472
+ else
473
+ n_d_hash[name] = default
474
+ end
475
+ n_o_hash[name] ||= i
476
+ end
477
+ temp_data.each do |datum|
478
+ unless n_d_hash.keys.include?(datum.name)
479
+ puts "WARNING: no default found for control #{datum.name}. Using standard defaults." if whine
480
+ end
481
+ default = n_d_hash[datum.name]
482
+ if default.is_a?(Hash) and datum.value.is_a?(Hash)
483
+ datum.value = datum.value.merge(default)
484
+ else
485
+ datum.value = default || datum.value
486
+ end
487
+ datum.order = n_o_hash[datum.name] || datum.order
488
+ end
489
+ temp_data
490
+ end
491
+
492
+ def self.full_line(lines, i)
493
+ return lines[i] unless lines[i][-1] == '&'
494
+ [lines[i].sub('&', ''), full_line(lines, i+1)].join(' ')
495
+ end
496
+
497
+ def self.is_comment?(line)
498
+ line =~ /\A\s*!/
499
+ end
500
+
501
+ def self.is_blank?(line)
502
+ not (line =~/[a-z0-9]+/)
503
+ end
504
+
505
+ def self.has_comment?(line)
506
+ line.include?('!')
507
+ end
508
+
509
+ # Making an instance of Inlist first checks to see if the class methods are
510
+ # set up for the namelists in Inlist.namelists. If they aren't ready, it
511
+ # creates them. Then creates a hash with an array associated to each namelist
512
+ # that is the exact size of the number of entries available in that namelist.
513
+
514
+ attr_accessor :data_hash
515
+ attr_reader :names
516
+ def initialize
517
+ Inlist.get_data unless Inlist.have_data?
518
+ @data = Inlist.inlist_data
519
+ @data_hash = {}
520
+ @data.each_value do |namelist_data|
521
+ namelist_data.each do |datum|
522
+ @data_hash[datum.name] = datum.dup
523
+
524
+ end
525
+ end
526
+ @names = @data_hash.keys
527
+ @data = {}
528
+ Inlist.namelists.each do |namelist|
529
+ @data[namelist] = Array.new(Inlist.inlist_data[namelist].size, '')
530
+ end
531
+ end
532
+
533
+ # Zeroes out all staged data and blank lines
534
+ def make_fresh_writelist
535
+ @to_write = {}
536
+ @data.keys.each do |namelist|
537
+ @to_write[namelist] = Array.new(@data[namelist].size, '')
538
+ end
539
+ end
540
+
541
+ def namelists
542
+ @data.keys
543
+ end
544
+
545
+ def flag_command(name)
546
+ @data_hash[name].flagged = true
547
+ end
548
+
549
+ def unflag_command(name)
550
+ @data_hash[name].flagged = false
551
+ end
552
+
553
+ def stage_namelist_command(name)
554
+ datum = @data_hash[name]
555
+ if datum.is_arr
556
+ lines = @data_hash[name].value.keys.map do |key|
557
+ prefix = " #{datum.name}("
558
+ suffix = ") = " +
559
+ Inlist.parse_input(datum.name, datum.value[key], datum.type) + "\n"
560
+ if key.respond_to?(:inject)
561
+ indices = key[1..-1].inject(key[0].to_s) do |res, elt|
562
+ "#{res}, #{elt}"
563
+ end
564
+ else
565
+ indices = key.to_s
566
+ end
567
+ prefix + indices + suffix
568
+ end
569
+ lines = lines.join
570
+ @to_write[datum.namelist][datum.order] = lines
571
+ else
572
+ @to_write[datum.namelist][datum.order] = " " + datum.name + ' = ' +
573
+ Inlist.parse_input(datum.name, datum.value, datum.type) + "\n"
574
+ end
575
+ end
576
+
577
+ # Marks a data category so that it can be staged into an inlist
578
+ def flagged
579
+ @data_hash.keys.select { |key| @data_hash[key].flagged }
580
+ end
581
+
582
+ # Collects all data categories into a hash of arrays (each array is a
583
+ # namelist) that is read whenever the inlist is converted to a string
584
+ # (i.e. when it is printed to a file or the screen).
585
+ def stage_flagged
586
+ make_fresh_writelist # start from scratch
587
+
588
+ flagged.each { |name| stage_namelist_command(name) } # stage each datum
589
+
590
+ # blank lines between disparate data
591
+ namelists.each do |namelist|
592
+ @to_write[namelist].each_index do |i|
593
+ next if (i == 0 or i == @to_write[namelist].size - 1)
594
+ this_line = @to_write[namelist][i]
595
+ prev_line = @to_write[namelist][i-1]
596
+
597
+ this_line = '' if this_line.nil?
598
+ prev_line = '' if prev_line.nil?
599
+ if this_line.empty? and not(prev_line.empty? or prev_line == "\n")
600
+ @to_write[namelist][i] = "\n"
601
+ end
602
+ end
603
+ end
604
+ end
605
+
606
+ # Takes the staged data categories and formats them into a string series of
607
+ # namelists that are MESA-readable.
608
+ def to_s
609
+ result = ''
610
+ namelists.each do |namelist|
611
+ result += "\n&#{namelist}\n"
612
+ result += @to_write[namelist].join("")
613
+ result += "\n/ ! end of #{namelist} namelist\n"
614
+ end
615
+ result.sub("\n\n\n", "\n\n")
616
+ end
617
+
618
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mesa_script
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - William Wolf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: MesaScript - a DSL for making dynamic inlists for the MESA stellar evolution
14
+ code.
15
+ email: wmwolf@physics.ucsb.edu
16
+ executables:
17
+ - inlist2mesascript
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - bin/inlist2mesascript
23
+ - lib/mesa_script.rb
24
+ homepage: https://wmwolf.github.io
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.2.2
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: MesaScript is a domain specific language (DSL) that allows the user to write
48
+ inlists for MESA that include variables, loops, conditionals, etc. For more detailed
49
+ instructions, see the readme on the github page at https://github.com/wmwolf/MesaScript This
50
+ software requires a relatively modern installation of MESA (version > 5596). It
51
+ has been tested on Ruby versions > 1.9, but there is no guarantee it will work on
52
+ older (or newer!) versions. Any bugs or requests should be sent to the author, Bill
53
+ Wolf, at wmwolf@physics.ucsb.edu.
54
+ test_files: []