mesa_script 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []