mesa-reader 0.3.3__py3-none-any.whl
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.
mesa_reader/__init__.py
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from os.path import join
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProfileError(Exception):
|
|
9
|
+
|
|
10
|
+
def __init__(self, msg):
|
|
11
|
+
Exception.__init__(self, msg)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HistoryError(Exception):
|
|
15
|
+
|
|
16
|
+
def __init__(self, msg):
|
|
17
|
+
Exception.__init__(self, msg)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelNumberError(Exception):
|
|
21
|
+
|
|
22
|
+
def __init__(self, msg):
|
|
23
|
+
Exception.__init__(self, msg)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BadPathError(Exception):
|
|
27
|
+
|
|
28
|
+
def __init__(self, msg):
|
|
29
|
+
Exception.__init__(self, msg)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnknownFileTypeError(Exception):
|
|
33
|
+
|
|
34
|
+
def __init__(self, msg):
|
|
35
|
+
Exception.__init__(self, msg)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MesaData:
|
|
39
|
+
|
|
40
|
+
"""Structure containing data from a Mesa output file.
|
|
41
|
+
|
|
42
|
+
Reads a profile or history output file from mesa. Assumes a file with
|
|
43
|
+
the following structure:
|
|
44
|
+
|
|
45
|
+
- line 1: header names
|
|
46
|
+
- line 2: header data
|
|
47
|
+
- line 3: blank
|
|
48
|
+
- line 4: main data names
|
|
49
|
+
- line 5: main data values
|
|
50
|
+
|
|
51
|
+
This structure can be altered by using the class methods
|
|
52
|
+
MesaData.set_header_rows and MesaData.set_data_rows.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
file_name : str, optional
|
|
57
|
+
File name to be read in. Default is 'LOGS/history.data', which works
|
|
58
|
+
for scripts in a standard work directory with a standard logs directory
|
|
59
|
+
for accessing the history data.
|
|
60
|
+
|
|
61
|
+
Attributes
|
|
62
|
+
----------
|
|
63
|
+
file_name : str
|
|
64
|
+
Path to file from which the data is read.
|
|
65
|
+
bulk_data : numpy.ndarray
|
|
66
|
+
The main data (line 6 and below) in record array format. Primarily
|
|
67
|
+
accessed via the `data` method.
|
|
68
|
+
bulk_names : tuple of str
|
|
69
|
+
Tuple of all available data column names that are valid inputs for
|
|
70
|
+
`data`. Essentially the column names in line 4 of `file_name`.
|
|
71
|
+
header_data : dict
|
|
72
|
+
Header data (line 2 of `file_name`) in dict format
|
|
73
|
+
header_names : list of str
|
|
74
|
+
List of all available header column names that are valid inputs for
|
|
75
|
+
`header`. Essentially the column names in line 1 of `file_name`.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
header_names_line = 2
|
|
79
|
+
bulk_names_line = 6
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def set_header_name_line(cls, name_line=2):
|
|
83
|
+
cls.header_names_line = name_line
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def set_data_rows(cls, name_line=6):
|
|
87
|
+
cls.bulk_names_line = name_line
|
|
88
|
+
|
|
89
|
+
# For pickle support
|
|
90
|
+
def __getstate__(self):
|
|
91
|
+
return (self.file_name, self.bulk_data, self.bulk_names, self.header_data, self.header_names)
|
|
92
|
+
|
|
93
|
+
# For pickle support
|
|
94
|
+
def __setstate__(self, state):
|
|
95
|
+
self.file_name, self.bulk_data, self.bulk_names, self.header_data, self.header_names = state
|
|
96
|
+
|
|
97
|
+
def __init__(self, file_name=join('.', 'LOGS', 'history.data'),
|
|
98
|
+
file_type=None):
|
|
99
|
+
"""Make a MesaData object from a Mesa output file.
|
|
100
|
+
|
|
101
|
+
Reads a profile or history output file from mesa. Assumes a file with
|
|
102
|
+
the following structure:
|
|
103
|
+
|
|
104
|
+
line 1: header names
|
|
105
|
+
line 2: header data
|
|
106
|
+
line 3: blank
|
|
107
|
+
line 4: main data names
|
|
108
|
+
line 5: main data values
|
|
109
|
+
|
|
110
|
+
This structure can be altered by using the class methods
|
|
111
|
+
`MesaData.set_header_rows` and `MesaData.set_data_rows`.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
file_name : str, optional
|
|
116
|
+
File name to be read in. Default is 'LOGS/history.data'
|
|
117
|
+
|
|
118
|
+
file_type : str, optional
|
|
119
|
+
File type of file to be read. Default is None, which will
|
|
120
|
+
auto-detect the type based on file extension. Valid values are
|
|
121
|
+
'model' (a saved model) and 'log' (history or profile output).
|
|
122
|
+
"""
|
|
123
|
+
self.file_name = file_name
|
|
124
|
+
self.file_type = file_type
|
|
125
|
+
self.bulk_data = None
|
|
126
|
+
self.bulk_names = None
|
|
127
|
+
self.header_data = None
|
|
128
|
+
self.header_names = None
|
|
129
|
+
self.read_data()
|
|
130
|
+
|
|
131
|
+
def __lt__(self, other):
|
|
132
|
+
try:
|
|
133
|
+
sage = float(self.star_age)
|
|
134
|
+
oage = float(other.star_age)
|
|
135
|
+
return sage < oage
|
|
136
|
+
except:
|
|
137
|
+
return self.file_name < other.file_name
|
|
138
|
+
|
|
139
|
+
def __str__(self):
|
|
140
|
+
try:
|
|
141
|
+
model_number = int(self.model_number)
|
|
142
|
+
age = float(self.star_age)
|
|
143
|
+
return "MESA model # {:6}, t = {:20.10g} yr".format(model_number, age)
|
|
144
|
+
except:
|
|
145
|
+
return "{}".format(self.file_name)
|
|
146
|
+
|
|
147
|
+
def read_data(self):
|
|
148
|
+
"""Decide if data file is log output or a model, then load the data
|
|
149
|
+
|
|
150
|
+
Log files and models are structured differently, so different methods
|
|
151
|
+
will be required to read in each. This method figures out which one
|
|
152
|
+
should be called and then punts to that method.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
None
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
None
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
# attempt auto-detection of file_type (if not supplied)
|
|
164
|
+
if self.file_type is None:
|
|
165
|
+
if self.file_name.endswith((".data", ".log")):
|
|
166
|
+
self.file_type = 'log'
|
|
167
|
+
elif self.file_name.endswith(".mod"):
|
|
168
|
+
self.file_type = 'model'
|
|
169
|
+
else:
|
|
170
|
+
raise UnknownFileTypeError("Unknown file type for file {}".format(
|
|
171
|
+
self.file_name))
|
|
172
|
+
|
|
173
|
+
# punt to reading method appropriate for each file type
|
|
174
|
+
if self.file_type == 'model':
|
|
175
|
+
self.read_model_data()
|
|
176
|
+
elif self.file_type == 'log':
|
|
177
|
+
self.read_log_data()
|
|
178
|
+
else:
|
|
179
|
+
raise UnknownFileTypeError("Unknown file type {}".format(
|
|
180
|
+
self.file_type))
|
|
181
|
+
|
|
182
|
+
def read_log_data(self):
|
|
183
|
+
"""Reads in or update data from the original log (.data or .log) file.
|
|
184
|
+
|
|
185
|
+
This re-reads the data from the originally-provided file name. Mostly
|
|
186
|
+
useful if the data file has been changed since it was first read in or
|
|
187
|
+
if the class methods MesaData.set_header_rows or MesaData.set_data_rows
|
|
188
|
+
have been used to alter how the data have been read in.
|
|
189
|
+
|
|
190
|
+
Returns
|
|
191
|
+
-------
|
|
192
|
+
None
|
|
193
|
+
"""
|
|
194
|
+
self.bulk_data = np.genfromtxt(
|
|
195
|
+
self.file_name, skip_header=MesaData.bulk_names_line - 1,
|
|
196
|
+
names=True, dtype=None)
|
|
197
|
+
self.bulk_names = self.bulk_data.dtype.names
|
|
198
|
+
header_data = []
|
|
199
|
+
with open(self.file_name) as f:
|
|
200
|
+
for i, line in enumerate(f):
|
|
201
|
+
if i == MesaData.header_names_line - 1:
|
|
202
|
+
self.header_names = line.split()
|
|
203
|
+
elif i == MesaData.header_names_line:
|
|
204
|
+
header_data = [eval(datum) for datum in line.split()]
|
|
205
|
+
elif i > MesaData.header_names_line:
|
|
206
|
+
break
|
|
207
|
+
self.header_data = dict(zip(self.header_names, header_data))
|
|
208
|
+
self.remove_backups()
|
|
209
|
+
|
|
210
|
+
def read_model_data(self):
|
|
211
|
+
"""Read in or update data from the original model (.mod) file.
|
|
212
|
+
|
|
213
|
+
Models are assumed to have the following structure:
|
|
214
|
+
|
|
215
|
+
- lines of comments and otherwise [considered] useless information
|
|
216
|
+
- one or more blank line
|
|
217
|
+
- Header information (names and values separated by one or more space, one per line)
|
|
218
|
+
- one or more blank lines
|
|
219
|
+
- ONE line of column names (strings separated by one or more spaces)
|
|
220
|
+
- many lines of bulk data (integer followed by many doubles, separated by one or more spaces)
|
|
221
|
+
- a blank line
|
|
222
|
+
- everything else is ignored
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
None
|
|
227
|
+
|
|
228
|
+
Returns
|
|
229
|
+
-------
|
|
230
|
+
None
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def pythonize_number(num_string):
|
|
234
|
+
"""Convert fotran double [string] to python readable number [string].
|
|
235
|
+
|
|
236
|
+
Converts numbers with exponential notation of D+, D-, d+, or d-
|
|
237
|
+
to E+ or E- so that a python interpreter properly understands them.
|
|
238
|
+
Leaves all other strings the untouched.
|
|
239
|
+
"""
|
|
240
|
+
num_string = re.sub('(d|D)\+', 'E+', num_string)
|
|
241
|
+
return re.sub('(d|D)-', 'E-', num_string)
|
|
242
|
+
|
|
243
|
+
with open(self.file_name, 'r') as f:
|
|
244
|
+
lines = f.readlines()
|
|
245
|
+
# Walk through file until we get to the last blank line, saving
|
|
246
|
+
# relevant data as we go.
|
|
247
|
+
blank_line_matcher = re.compile('^\s*$')
|
|
248
|
+
i = 0
|
|
249
|
+
found_blank_line = False
|
|
250
|
+
while not found_blank_line:
|
|
251
|
+
i += 1
|
|
252
|
+
found_blank_line = (blank_line_matcher.match(lines[i]) is not None)
|
|
253
|
+
# now on blank line 1, advance through one or more lines to get to
|
|
254
|
+
# header data
|
|
255
|
+
while found_blank_line:
|
|
256
|
+
i += 1
|
|
257
|
+
found_blank_line = (blank_line_matcher.match(lines[i]) is not None)
|
|
258
|
+
# now done with blank lines and on to header data
|
|
259
|
+
self.header_names = []
|
|
260
|
+
self.header_data = {}
|
|
261
|
+
while not found_blank_line:
|
|
262
|
+
name, val = [datum.strip() for datum in lines[i].split()[:2]]
|
|
263
|
+
self.header_data[name] = eval(pythonize_number(val))
|
|
264
|
+
self.header_names.append(name)
|
|
265
|
+
i += 1
|
|
266
|
+
found_blank_line = (blank_line_matcher.match(lines[i]) is not None)
|
|
267
|
+
# now on blank line 2, advance until we get to column names
|
|
268
|
+
while found_blank_line:
|
|
269
|
+
i += 1
|
|
270
|
+
found_blank_line = (blank_line_matcher.match(lines[i]) is not None)
|
|
271
|
+
self.bulk_names = ['zone']
|
|
272
|
+
self.bulk_names += lines[i].split()
|
|
273
|
+
i += 1
|
|
274
|
+
self.bulk_data = {}
|
|
275
|
+
temp_data = []
|
|
276
|
+
found_blank_line = False
|
|
277
|
+
while not found_blank_line:
|
|
278
|
+
temp_data.append([eval(pythonize_number(datum)) for datum in
|
|
279
|
+
lines[i].split()])
|
|
280
|
+
i += 1
|
|
281
|
+
found_blank_line = (blank_line_matcher.match(lines[i]) is not None)
|
|
282
|
+
temp_data = np.array(temp_data).T
|
|
283
|
+
for i in range(len(self.bulk_names)):
|
|
284
|
+
self.bulk_data[self.bulk_names[i]] = temp_data[i]
|
|
285
|
+
# self.bulk_data = np.array(temp_data, names=self.bulk_names)
|
|
286
|
+
|
|
287
|
+
def data(self, key):
|
|
288
|
+
"""Accesses the data and returns a numpy array with the appropriate data
|
|
289
|
+
|
|
290
|
+
Accepts a string key, like star_age (for history files) or logRho (for
|
|
291
|
+
profile files) and returns the corresponding numpy array of data for
|
|
292
|
+
that data type. Can also just use the shorthand methods that have the
|
|
293
|
+
same name of the key.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
key : str
|
|
298
|
+
Name of data. Must match a main data title in the source file. If it
|
|
299
|
+
is not a data title, will first check for a log_[`key`] or ln[`key`]
|
|
300
|
+
version and return an exponentiated version of that data. If `key`
|
|
301
|
+
looks like a `log_*` or `ln_*` name, searches for a linear
|
|
302
|
+
quantity of the appropriate name and returns the expected
|
|
303
|
+
logarithmic quantity.
|
|
304
|
+
|
|
305
|
+
Returns
|
|
306
|
+
-------
|
|
307
|
+
numpy.ndarray
|
|
308
|
+
Array of values for data corresponding to key at various time steps
|
|
309
|
+
(history) or grid points (profile or model).
|
|
310
|
+
|
|
311
|
+
Raises
|
|
312
|
+
------
|
|
313
|
+
KeyError
|
|
314
|
+
If `key` is an invalid key (i.e. not in `self.bulk_names` and no
|
|
315
|
+
fallback logarithmic or linear quantities found)
|
|
316
|
+
|
|
317
|
+
Examples
|
|
318
|
+
--------
|
|
319
|
+
You can either call `data` explicitly with `key` as an argument, or get
|
|
320
|
+
the same result by calling it implicitly by treating `key` as an
|
|
321
|
+
attribute.
|
|
322
|
+
|
|
323
|
+
>>> m = MesaData()
|
|
324
|
+
>>> x = m.data('star_age')
|
|
325
|
+
>>> y = m.star_age
|
|
326
|
+
>>> x == y
|
|
327
|
+
True
|
|
328
|
+
|
|
329
|
+
In this case, x and y are the same because the non-existent method
|
|
330
|
+
MesaData.star_age will direct to to the corresponding MesaData.data
|
|
331
|
+
call.
|
|
332
|
+
|
|
333
|
+
Even data categories that are not in the file may still work.
|
|
334
|
+
Specifically, if a linear quantity is available, but the log is asked
|
|
335
|
+
for, the linear quantity will be first log-ified and then returned:
|
|
336
|
+
|
|
337
|
+
>>> m = MesaData()
|
|
338
|
+
>>> m.in_data('L')
|
|
339
|
+
False
|
|
340
|
+
>>> m.in_data('log_L')
|
|
341
|
+
True
|
|
342
|
+
>>> x = m.L
|
|
343
|
+
>>> y = 10**m.log_L
|
|
344
|
+
>>> x == y
|
|
345
|
+
True
|
|
346
|
+
|
|
347
|
+
Here, `data` was called implicitly with an argument of 'L' to get `x`.
|
|
348
|
+
Since `'L'` was an invalid data category, it first looked to see if a
|
|
349
|
+
logarithmic version existed. Indeed, `'log_L'` was present, so it was
|
|
350
|
+
retrieved, exponentiated, and returned.
|
|
351
|
+
"""
|
|
352
|
+
if self.in_data(key):
|
|
353
|
+
return self.bulk_data[key]
|
|
354
|
+
elif self._log_version(key) is not None:
|
|
355
|
+
return 10**self.bulk_data[self._log_version(key)]
|
|
356
|
+
elif self._ln_version(key) is not None:
|
|
357
|
+
return np.exp(self.bulk_data[self._ln_version(key)])
|
|
358
|
+
elif self._exp10_version(key) is not None:
|
|
359
|
+
return np.log10(self.bulk_data[self._exp10_version(key)])
|
|
360
|
+
elif self._exp_version(key) is not None:
|
|
361
|
+
return np.log(self.bulk_data[self._exp_version(key)])
|
|
362
|
+
else:
|
|
363
|
+
raise KeyError("'" + str(key) + "' is not a valid data type.")
|
|
364
|
+
|
|
365
|
+
def header(self, key):
|
|
366
|
+
"""Accesses the header, returning a scalar the appropriate data
|
|
367
|
+
|
|
368
|
+
Accepts a string key, like version_number and returns the corresponding
|
|
369
|
+
datum for that key. Can also just use the shorthand
|
|
370
|
+
methods that have the same name of the key.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
key : string
|
|
375
|
+
Name of data. Must match a main data title in the source file.
|
|
376
|
+
|
|
377
|
+
Returns
|
|
378
|
+
-------
|
|
379
|
+
int or str or float
|
|
380
|
+
Returns whatever value is below the corresponding key in the header
|
|
381
|
+
lines of the source file.
|
|
382
|
+
|
|
383
|
+
Raises
|
|
384
|
+
------
|
|
385
|
+
KeyError
|
|
386
|
+
If `key` is an invalid key (i.e. not in `self.header_names`)
|
|
387
|
+
|
|
388
|
+
Examples
|
|
389
|
+
--------
|
|
390
|
+
Can call `header` explicitly with `key` as argument or implicitly,
|
|
391
|
+
treating `key` as an attribute.
|
|
392
|
+
|
|
393
|
+
In this case, x and y are the same because the non-existent method
|
|
394
|
+
MesaData.version_number will first see if it can call
|
|
395
|
+
MesaData.data('version_number'). Then, realizing that this doesn't make
|
|
396
|
+
sense, it will instead call MesaData.header('version_number')
|
|
397
|
+
|
|
398
|
+
>>> m = MesaData()
|
|
399
|
+
>>> x = m.data('version_number')
|
|
400
|
+
>>> y = m.version_number
|
|
401
|
+
>>> x == y
|
|
402
|
+
True
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
if not self.in_header(key):
|
|
406
|
+
raise KeyError("'" + str(key) + "' is not a valid header name.")
|
|
407
|
+
return self.header_data[key]
|
|
408
|
+
|
|
409
|
+
def is_history(self):
|
|
410
|
+
"""Determine if the source file is a history file
|
|
411
|
+
|
|
412
|
+
Checks if 'model_number' is a valid key for self.data. If it is, return
|
|
413
|
+
True. Otherwise return False. This is used in determining whether or not
|
|
414
|
+
to cleanse the file of backups and restarts in the MesaData.read_data.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
bool
|
|
419
|
+
True if file is a history file, otherwise False
|
|
420
|
+
"""
|
|
421
|
+
return 'model_number' in self.bulk_names
|
|
422
|
+
|
|
423
|
+
def in_header(self, key):
|
|
424
|
+
"""Determine if `key` is an available header data category.
|
|
425
|
+
|
|
426
|
+
Checks if string `key` is a valid argument of MesaData.header. Returns
|
|
427
|
+
True if it is, otherwise False
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
key : str
|
|
432
|
+
Candidate string for accessing header data. This is what you want
|
|
433
|
+
to be able to use as an argument of `MesaData.header`.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
bool
|
|
438
|
+
True if `key` is a valid input to MesaData.header, otherwise False.
|
|
439
|
+
|
|
440
|
+
Notes
|
|
441
|
+
-----
|
|
442
|
+
This is automatically called by MesaData.header, so the average user
|
|
443
|
+
shouldn't need to call it.
|
|
444
|
+
"""
|
|
445
|
+
return key in self.header_names
|
|
446
|
+
|
|
447
|
+
def in_data(self, key):
|
|
448
|
+
"""Determine if `key` is an available main data category.
|
|
449
|
+
|
|
450
|
+
Checks if string `key` is a valid argument of MesaData.data. Returns
|
|
451
|
+
True if it is, otherwise False
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
key : str
|
|
456
|
+
Candidate string for accessing main data. This is what you want
|
|
457
|
+
to be able to use as an argument of MesaData.data.
|
|
458
|
+
|
|
459
|
+
Returns
|
|
460
|
+
-------
|
|
461
|
+
bool
|
|
462
|
+
True if `key` is a valid input to MesaData.data, otherwise False.
|
|
463
|
+
|
|
464
|
+
Notes
|
|
465
|
+
-----
|
|
466
|
+
This is automatically called by MesaData.data, so the average user
|
|
467
|
+
shouldn't need to call it.
|
|
468
|
+
"""
|
|
469
|
+
return key in self.bulk_names
|
|
470
|
+
|
|
471
|
+
def _log_version(self, key):
|
|
472
|
+
"""Determine if the log of the desired value is available and return it.
|
|
473
|
+
|
|
474
|
+
If a log_10 version of the value desired is found in the data columns,
|
|
475
|
+
the "logified" name will be returned. Otherwise it will return `None`.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
key : str
|
|
480
|
+
Candidate string for accessing main data. This is what you want
|
|
481
|
+
to be able to use as an argument of MesaData.data.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
str or `None`
|
|
486
|
+
The "logified" version of the key, if available. If unavailable,
|
|
487
|
+
`None`.
|
|
488
|
+
"""
|
|
489
|
+
log_prefixes = ['log_', 'log', 'lg_', 'lg']
|
|
490
|
+
for prefix in log_prefixes:
|
|
491
|
+
if self.in_data(prefix + key):
|
|
492
|
+
return prefix + key
|
|
493
|
+
|
|
494
|
+
def _ln_version(self, key):
|
|
495
|
+
"""Determine if the ln of the desired value is available and return it.
|
|
496
|
+
|
|
497
|
+
If a log_e version of the value desired is found in the data columns,
|
|
498
|
+
the "ln-ified" name will be returned. Otherwise it will return `None`.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
key : str
|
|
503
|
+
Candidate string for accessing main data. This is what you want
|
|
504
|
+
to be able to use as an argument of MesaData.data.
|
|
505
|
+
|
|
506
|
+
Returns
|
|
507
|
+
-------
|
|
508
|
+
str or `None`
|
|
509
|
+
The "ln-ified" version of the key, if available. If unavailable,
|
|
510
|
+
`None`.
|
|
511
|
+
"""
|
|
512
|
+
log_prefixes = ['ln_', 'ln']
|
|
513
|
+
for prefix in log_prefixes:
|
|
514
|
+
if self.in_data(prefix + key):
|
|
515
|
+
return prefix + key
|
|
516
|
+
|
|
517
|
+
def _exp10_version(self, key):
|
|
518
|
+
"""Find if the non-log version of a value is available and return it
|
|
519
|
+
|
|
520
|
+
If a non-log version of the value desired is found in the data columns,
|
|
521
|
+
the linear name will be returned. Otherwise it will return `None`.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
key : str
|
|
526
|
+
Candidate string for accessing main data. This is what you want
|
|
527
|
+
to be able to use as an argument of MesaData.data.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
str or `None`
|
|
532
|
+
The linear version of the key, if available. If unavailable, `None`.
|
|
533
|
+
"""
|
|
534
|
+
log_matcher = re.compile('^lo?g_?(.+)')
|
|
535
|
+
matches = log_matcher.match(key)
|
|
536
|
+
if matches is not None:
|
|
537
|
+
groups = matches.groups()
|
|
538
|
+
if self.in_data(groups[0]):
|
|
539
|
+
return groups[0]
|
|
540
|
+
|
|
541
|
+
def _exp_version(self, key):
|
|
542
|
+
"""Find if the non-ln version of a value is available and return it
|
|
543
|
+
|
|
544
|
+
If a non-ln version of the value desired is found in the data columns,
|
|
545
|
+
the linear name will be returned. Otherwise it will return `None`.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
key : str
|
|
550
|
+
Candidate string for accessing main data. This is what you want
|
|
551
|
+
to be able to use as an argument of MesaData.data.
|
|
552
|
+
|
|
553
|
+
Returns
|
|
554
|
+
-------
|
|
555
|
+
str or `None`
|
|
556
|
+
The linear version of the key, if available. If unavailable, `None`.
|
|
557
|
+
"""
|
|
558
|
+
log_matcher = re.compile('^ln_?(.+)')
|
|
559
|
+
matches = log_matcher.match(key)
|
|
560
|
+
if matches is not None:
|
|
561
|
+
groups = matches.groups()
|
|
562
|
+
if self.in_data(groups[0]):
|
|
563
|
+
return groups[0]
|
|
564
|
+
|
|
565
|
+
def _any_version(self, key):
|
|
566
|
+
"""Determine if `key` can point to a valid data category
|
|
567
|
+
|
|
568
|
+
Parameters
|
|
569
|
+
----------
|
|
570
|
+
key : str
|
|
571
|
+
Candidate string for accessing main data. This is what you want
|
|
572
|
+
to be able to use as an argument of MesaData.data.
|
|
573
|
+
|
|
574
|
+
Returns
|
|
575
|
+
-------
|
|
576
|
+
bool
|
|
577
|
+
True if `key` can be mapped to a data type either directly or by
|
|
578
|
+
exponentiating/taking logarithms of existing data types
|
|
579
|
+
"""
|
|
580
|
+
return bool(self.in_data(key) or self._log_version(key) or
|
|
581
|
+
self._ln_version(key) or self._exp_version(key) or
|
|
582
|
+
self._exp10_version(key))
|
|
583
|
+
|
|
584
|
+
def data_at_model_number(self, key, m_num):
|
|
585
|
+
"""Return main data at a specific model number (for history files).
|
|
586
|
+
|
|
587
|
+
Finds the index i where MesaData.data('model_number')[i] == m_num. Then
|
|
588
|
+
returns MesaData.data(key)[i]. Essentially lets you use model numbers
|
|
589
|
+
to index data.
|
|
590
|
+
|
|
591
|
+
Parameters
|
|
592
|
+
----------
|
|
593
|
+
key : str
|
|
594
|
+
Name of data. Must match a main data title in the source file.
|
|
595
|
+
|
|
596
|
+
m_num : int
|
|
597
|
+
Model number where you want to sample the data
|
|
598
|
+
|
|
599
|
+
Returns
|
|
600
|
+
-------
|
|
601
|
+
float or int
|
|
602
|
+
Value of MesaData.data(`key`) at the same index where
|
|
603
|
+
MesaData.data('model_number') == `m_num`
|
|
604
|
+
|
|
605
|
+
See Also
|
|
606
|
+
--------
|
|
607
|
+
index_of_model_number : returns the index for sampling, not the value
|
|
608
|
+
"""
|
|
609
|
+
return self.data(key)[self.index_of_model_number(m_num)]
|
|
610
|
+
|
|
611
|
+
def index_of_model_number(self, m_num):
|
|
612
|
+
"""Return index where MesaData.data('model_number') is `m_num`.
|
|
613
|
+
|
|
614
|
+
Returns the index i where MesaData.data('model_number')[i] == m_num.
|
|
615
|
+
|
|
616
|
+
Parameters
|
|
617
|
+
----------
|
|
618
|
+
m_num : int
|
|
619
|
+
Model number where you want to sample data
|
|
620
|
+
|
|
621
|
+
Returns
|
|
622
|
+
-------
|
|
623
|
+
int
|
|
624
|
+
The index where MesaData.data('model_number') == `m_num`
|
|
625
|
+
|
|
626
|
+
Raises
|
|
627
|
+
------
|
|
628
|
+
HistoryError
|
|
629
|
+
If trying to access a non-history file
|
|
630
|
+
|
|
631
|
+
ModelNumberError
|
|
632
|
+
If zero or more than one model numbers matching `m_num` are found.
|
|
633
|
+
|
|
634
|
+
See Also
|
|
635
|
+
--------
|
|
636
|
+
data_at_model_number : returns the datum of a specific key a model no.
|
|
637
|
+
"""
|
|
638
|
+
if not self.is_history():
|
|
639
|
+
raise HistoryError("Can't get data at model number " +
|
|
640
|
+
"because this isn't a history file")
|
|
641
|
+
index = np.where(self.data('model_number') == m_num)[0]
|
|
642
|
+
if len(index) > 1:
|
|
643
|
+
raise ModelNumberError("Found more than one entry where model " +
|
|
644
|
+
"number is " + str(m_num) + " in " +
|
|
645
|
+
self.file_name + ". Report this.")
|
|
646
|
+
elif len(index) == 0:
|
|
647
|
+
raise ModelNumberError("Couldn't find any entries with model " +
|
|
648
|
+
"number " + str(m_num) + ".")
|
|
649
|
+
elif len(index) == 1:
|
|
650
|
+
return index[0]
|
|
651
|
+
|
|
652
|
+
def remove_backups(self, dbg=False):
|
|
653
|
+
"""Cleanses a history file of backups and restarts
|
|
654
|
+
|
|
655
|
+
If the file is a history file, goes through and ensure that the
|
|
656
|
+
model_number data are monotonically increasing. It removes rows of data
|
|
657
|
+
from all categories if there are earlier ones later in the file.
|
|
658
|
+
|
|
659
|
+
Parameters
|
|
660
|
+
----------
|
|
661
|
+
dbg : bool, optional
|
|
662
|
+
If True, will output how many lines are cleansed. Default is False
|
|
663
|
+
|
|
664
|
+
Returns
|
|
665
|
+
-------
|
|
666
|
+
None
|
|
667
|
+
"""
|
|
668
|
+
if not self.is_history():
|
|
669
|
+
return None
|
|
670
|
+
if dbg:
|
|
671
|
+
print("Scrubbing history...")
|
|
672
|
+
to_remove = []
|
|
673
|
+
for i in range(len(self.data('model_number')) - 1):
|
|
674
|
+
smallest_future = np.min(self.data('model_number')[i + 1:])
|
|
675
|
+
if self.data('model_number')[i] >= smallest_future:
|
|
676
|
+
to_remove.append(i)
|
|
677
|
+
if len(to_remove) == 0:
|
|
678
|
+
if dbg:
|
|
679
|
+
print("Already clean!")
|
|
680
|
+
return None
|
|
681
|
+
if dbg:
|
|
682
|
+
print("Removing {} lines.".format(len(to_remove)))
|
|
683
|
+
self.bulk_data = np.delete(self.bulk_data, to_remove)
|
|
684
|
+
|
|
685
|
+
def __getattr__(self, method_name):
|
|
686
|
+
if self._any_version(method_name):
|
|
687
|
+
return self.data(method_name)
|
|
688
|
+
elif self.in_header(method_name):
|
|
689
|
+
return self.header(method_name)
|
|
690
|
+
else:
|
|
691
|
+
raise AttributeError(method_name)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class MesaProfileIndex:
|
|
695
|
+
|
|
696
|
+
"""Structure containing data from the profile index from MESA output.
|
|
697
|
+
|
|
698
|
+
Reads in data from profile index file from MESA, allowing a mapping from
|
|
699
|
+
profile number to model number and vice versa. Mostly accessed via the
|
|
700
|
+
MesaLogDir class.
|
|
701
|
+
|
|
702
|
+
Parameters
|
|
703
|
+
----------
|
|
704
|
+
file_name : str, optional
|
|
705
|
+
Path to the profile index file to be read in. Default is
|
|
706
|
+
'LOGS/profiles.index', which should work when the working directory is
|
|
707
|
+
a standard work directory and the logs directory is of the default
|
|
708
|
+
name.
|
|
709
|
+
|
|
710
|
+
Attributes
|
|
711
|
+
----------
|
|
712
|
+
file_name : str
|
|
713
|
+
path to the profile index file
|
|
714
|
+
index_data : dict
|
|
715
|
+
dictionary containing all index data in numpy arrays.
|
|
716
|
+
model_number_string : str
|
|
717
|
+
header name of the model number column in `file_name`
|
|
718
|
+
profile_number_string : str
|
|
719
|
+
header name of the profile number column in `file_name`
|
|
720
|
+
profile_numbers : numpy.ndarray
|
|
721
|
+
List of all available profile numbers in order of their corresponding
|
|
722
|
+
model numbers (i.e. time-order).
|
|
723
|
+
model_numbers : numpy.ndarray
|
|
724
|
+
Sorted list of all available model numbers.
|
|
725
|
+
"""
|
|
726
|
+
|
|
727
|
+
index_start_line = 2
|
|
728
|
+
index_end = None
|
|
729
|
+
index_names = ['model_numbers', 'priorities', 'profile_numbers']
|
|
730
|
+
|
|
731
|
+
@classmethod
|
|
732
|
+
def set_index_rows(cls, index_start=2, index_end=None):
|
|
733
|
+
cls.index_start_line = index_start
|
|
734
|
+
cls.index_end_line = index_end
|
|
735
|
+
return index_start, index_end
|
|
736
|
+
|
|
737
|
+
@classmethod
|
|
738
|
+
def set_index_names(cls, name_arr):
|
|
739
|
+
cls.index_names = name_arr
|
|
740
|
+
return name_arr
|
|
741
|
+
|
|
742
|
+
def __init__(self, file_name=join('.', 'LOGS', 'profiles.index')):
|
|
743
|
+
self.file_name = file_name
|
|
744
|
+
self.index_data = None
|
|
745
|
+
self.model_number_string = ''
|
|
746
|
+
self.profile_number_string = ''
|
|
747
|
+
self.profile_numbers = None
|
|
748
|
+
self.model_numbers = None
|
|
749
|
+
self.read_index()
|
|
750
|
+
|
|
751
|
+
def read_index(self):
|
|
752
|
+
"""Read (or re-read) data from `self.file_name`.
|
|
753
|
+
|
|
754
|
+
Read the file into an numpy array, sorting the table in order of
|
|
755
|
+
increasing model numbers and establishes the `profile_numbers` and
|
|
756
|
+
`model_numbers` attributes. Converts data and names into a dictionary.
|
|
757
|
+
Called automatically at instantiation, but may be called again to
|
|
758
|
+
refresh data.
|
|
759
|
+
|
|
760
|
+
Returns
|
|
761
|
+
-------
|
|
762
|
+
None
|
|
763
|
+
"""
|
|
764
|
+
temp_index_data = np.genfromtxt(
|
|
765
|
+
self.file_name, skip_header=MesaProfileIndex.index_start_line - 1,
|
|
766
|
+
dtype=None)
|
|
767
|
+
self.model_number_string = MesaProfileIndex.index_names[0]
|
|
768
|
+
self.profile_number_string = MesaProfileIndex.index_names[-1]
|
|
769
|
+
self.index_data = temp_index_data[np.argsort(temp_index_data[:, 0])]
|
|
770
|
+
self.index_data = dict(zip(MesaProfileIndex.index_names,
|
|
771
|
+
temp_index_data.T))
|
|
772
|
+
self.profile_numbers = self.data(self.profile_number_string)
|
|
773
|
+
self.model_numbers = self.data(self.model_number_string)
|
|
774
|
+
|
|
775
|
+
def data(self, key):
|
|
776
|
+
"""Access index data and return array of column corresponding to `key`.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
key : str
|
|
781
|
+
Name of column to be returned. Likely choices are 'model_numbers',
|
|
782
|
+
'profile_numbers', or 'priorities'.
|
|
783
|
+
|
|
784
|
+
Returns
|
|
785
|
+
-------
|
|
786
|
+
numpy.ndarray
|
|
787
|
+
Array containing the data requested.
|
|
788
|
+
|
|
789
|
+
Raises
|
|
790
|
+
------
|
|
791
|
+
KeyError
|
|
792
|
+
If input key is not a valid column header name.
|
|
793
|
+
"""
|
|
794
|
+
if key not in self.index_names:
|
|
795
|
+
raise KeyError("'" + str(key) + "' is not a column in " +
|
|
796
|
+
self.file_name)
|
|
797
|
+
return np.array(self.index_data[key])
|
|
798
|
+
|
|
799
|
+
def have_profile_with_model_number(self, model_number):
|
|
800
|
+
"""Determines if given `model_number` has a matching profile number.
|
|
801
|
+
|
|
802
|
+
Attributes
|
|
803
|
+
----------
|
|
804
|
+
model_number : int
|
|
805
|
+
model number to be checked for available profile number
|
|
806
|
+
|
|
807
|
+
Returns
|
|
808
|
+
-------
|
|
809
|
+
bool
|
|
810
|
+
True if `model_number` has a corresponding profile number. False
|
|
811
|
+
otherwise.
|
|
812
|
+
"""
|
|
813
|
+
return model_number in self.data(self.model_number_string)
|
|
814
|
+
|
|
815
|
+
def have_profile_with_profile_number(self, profile_number):
|
|
816
|
+
"""Determines if given `profile_number` is a valid profile number.
|
|
817
|
+
|
|
818
|
+
Attributes
|
|
819
|
+
----------
|
|
820
|
+
profile_number : int
|
|
821
|
+
profile number to be verified
|
|
822
|
+
|
|
823
|
+
Returns
|
|
824
|
+
-------
|
|
825
|
+
bool
|
|
826
|
+
True if `profile_number` has a corresponding entry in the index.
|
|
827
|
+
False otherwise.
|
|
828
|
+
"""
|
|
829
|
+
return profile_number in self.data(self.profile_number_string)
|
|
830
|
+
|
|
831
|
+
def profile_with_model_number(self, model_number):
|
|
832
|
+
"""Converts a model number to a profile number if possible.
|
|
833
|
+
|
|
834
|
+
If `model_number` has a corresponding profile number in the index,
|
|
835
|
+
returns it. Otherwise throws an error.
|
|
836
|
+
|
|
837
|
+
Attributes
|
|
838
|
+
----------
|
|
839
|
+
model_number : int
|
|
840
|
+
model number to be converted into a profile number
|
|
841
|
+
|
|
842
|
+
Returns
|
|
843
|
+
-------
|
|
844
|
+
int
|
|
845
|
+
profile number corresponding to `model_number`
|
|
846
|
+
|
|
847
|
+
Raises
|
|
848
|
+
------
|
|
849
|
+
ProfileError
|
|
850
|
+
If no profile number can be found that corresponds to
|
|
851
|
+
`model_number`
|
|
852
|
+
"""
|
|
853
|
+
if not (self.have_profile_with_model_number(model_number)):
|
|
854
|
+
raise ProfileError("No profile with model number " +
|
|
855
|
+
str(model_number) + ".")
|
|
856
|
+
indices = np.where(self.data(self.model_number_string) == model_number)
|
|
857
|
+
return np.take(self.data(self.profile_number_string), indices[0])[0]
|
|
858
|
+
|
|
859
|
+
def model_with_profile_number(self, profile_number):
|
|
860
|
+
"""Converts a profile number to a profile number if possible.
|
|
861
|
+
|
|
862
|
+
If `profile_number` has a corresponding model number in the index,
|
|
863
|
+
returns it. Otherwise throws an error.
|
|
864
|
+
|
|
865
|
+
Attributes
|
|
866
|
+
----------
|
|
867
|
+
profile_number : int
|
|
868
|
+
profile number to be converted into a model number
|
|
869
|
+
|
|
870
|
+
Returns
|
|
871
|
+
-------
|
|
872
|
+
int
|
|
873
|
+
model number corresponding to `profile_number`
|
|
874
|
+
|
|
875
|
+
Raises
|
|
876
|
+
------
|
|
877
|
+
ProfileError
|
|
878
|
+
If no model number can be found that corresponds to
|
|
879
|
+
`profile_number`
|
|
880
|
+
"""
|
|
881
|
+
if not (self.have_profile_with_profile_number(profile_number)):
|
|
882
|
+
raise ProfileError("No Profile with profile number " +
|
|
883
|
+
str(profile_number) + ".")
|
|
884
|
+
indices = np.where(
|
|
885
|
+
self.data(self.profile_number_string) == profile_number)
|
|
886
|
+
return np.take(self.data(self.model_number_string), indices[0])[0]
|
|
887
|
+
|
|
888
|
+
def __getattr__(self, method_name):
|
|
889
|
+
if method_name in self.index_data.keys():
|
|
890
|
+
return self.data(method_name)
|
|
891
|
+
else:
|
|
892
|
+
raise AttributeError(method_name)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class MesaLogDir:
|
|
896
|
+
|
|
897
|
+
"""Structure providing access to both history and profile output from MESA
|
|
898
|
+
|
|
899
|
+
Provides access for accessing the history and profile data of a MESA run
|
|
900
|
+
by linking profiles to the history through model numbers.
|
|
901
|
+
|
|
902
|
+
Parameters
|
|
903
|
+
----------
|
|
904
|
+
log_path : str, optional
|
|
905
|
+
Path to the logs directory, default is 'LOGS'
|
|
906
|
+
profile_prefix : str, optional
|
|
907
|
+
Prefix before profile number in profile file names, default is
|
|
908
|
+
'profile'
|
|
909
|
+
profile_suffix : str, optional
|
|
910
|
+
Suffix after profile number and period for profile file names, default
|
|
911
|
+
is 'data'
|
|
912
|
+
history_file : str, optional
|
|
913
|
+
Name of the history file in the logs directory, default is
|
|
914
|
+
'history.data'
|
|
915
|
+
index_file : str, optional
|
|
916
|
+
Name of the profiles index file in the logs directory, default is
|
|
917
|
+
'profiles.index'
|
|
918
|
+
memoize_profiles : bool, optional
|
|
919
|
+
Determines whether or not profiles will be "memo-ized", default is
|
|
920
|
+
True. If memoized, once a profile is called into existence, it is saved
|
|
921
|
+
so that it need not be read in again. Good for quick, clean, repeated
|
|
922
|
+
access of a profile, but bad for reading in many profiles for one-time
|
|
923
|
+
uses as it will hog memory.
|
|
924
|
+
|
|
925
|
+
Attributes
|
|
926
|
+
-----------
|
|
927
|
+
log_path : str
|
|
928
|
+
Path to the logs directory; used (re-)reading data in
|
|
929
|
+
profile_prefix : str
|
|
930
|
+
Prefix before profile number in profile file names
|
|
931
|
+
profile_suffix : str
|
|
932
|
+
Suffix after profile number and period for profile file names
|
|
933
|
+
history_file : str
|
|
934
|
+
Base name (not path) of the history file in the logs directory
|
|
935
|
+
index_file : str
|
|
936
|
+
Base name (not path) of the profiles index file in the logs directory
|
|
937
|
+
memoize_profiles : bool
|
|
938
|
+
Determines whether or not profiles will be "memo-ized". Setting this
|
|
939
|
+
after initialization will not delete profiles from memory. It will
|
|
940
|
+
simply start/stop memoizing them. To clear out memoized profiles,
|
|
941
|
+
re-read the data with `self.read_logs()`
|
|
942
|
+
history_path : str
|
|
943
|
+
Path to the history data file
|
|
944
|
+
index_path : str
|
|
945
|
+
Path to the profile index file
|
|
946
|
+
history : mesa_reader.MesaData
|
|
947
|
+
MesaData object containing history information from `self.history_path`
|
|
948
|
+
history_data : mesa_reader.MesaData
|
|
949
|
+
Alias for `self.history`
|
|
950
|
+
profiles : mesa_reader.MesaProfileIndex
|
|
951
|
+
MesaProfileIndex from profiles in `self.index_path`
|
|
952
|
+
profile_numbers : numpy.ndarray
|
|
953
|
+
Result of calling `self.profiles.profile_numbers`. Just the profile
|
|
954
|
+
numbers of the simulation in order of corresponding model numbers.
|
|
955
|
+
model_numbers : numpy.ndarray
|
|
956
|
+
Result of calling `self.profiles.model_numbers`. Just the model numbers
|
|
957
|
+
of the simulations that have corresponding profiles in ascending order.
|
|
958
|
+
profile_dict : dict
|
|
959
|
+
Stores MesaData objects from profiles. Keys to this dictionary are
|
|
960
|
+
profile numbers, so presumably `self.profile_dict(5)` would yield the
|
|
961
|
+
MesaData object obtained from the file `profile5.data` (assuming
|
|
962
|
+
reasonable defaults) if such a profile was ever accessed. Will remain
|
|
963
|
+
empty if memoization is shut off.
|
|
964
|
+
"""
|
|
965
|
+
|
|
966
|
+
def __init__(self, log_path='LOGS', profile_prefix='profile',
|
|
967
|
+
profile_suffix='data', history_file='history.data',
|
|
968
|
+
index_file='profiles.index', memoize_profiles=True):
|
|
969
|
+
self.log_path = log_path
|
|
970
|
+
self.profile_prefix = profile_prefix
|
|
971
|
+
self.profile_suffix = profile_suffix
|
|
972
|
+
self.history_file = history_file
|
|
973
|
+
self.index_file = index_file
|
|
974
|
+
|
|
975
|
+
# Check if log_path and files are dir/files.
|
|
976
|
+
if not os.path.isdir(self.log_path):
|
|
977
|
+
raise BadPathError(self.log_path + ' is not a valid directory.')
|
|
978
|
+
|
|
979
|
+
self.history_path = os.path.join(self.log_path, self.history_file)
|
|
980
|
+
if not os.path.isfile(self.history_path):
|
|
981
|
+
raise BadPathError(self.history_file + ' not found in ' +
|
|
982
|
+
self.log_path + '.')
|
|
983
|
+
|
|
984
|
+
self.index_path = os.path.join(self.log_path, self.index_file)
|
|
985
|
+
if not os.path.isfile(self.index_path):
|
|
986
|
+
raise BadPathError(self.index_file + ' not found in ' +
|
|
987
|
+
self.log_path + '.')
|
|
988
|
+
|
|
989
|
+
self.memoize_profiles = memoize_profiles
|
|
990
|
+
|
|
991
|
+
self.history = None
|
|
992
|
+
self.history_data = None
|
|
993
|
+
self.profiles = None
|
|
994
|
+
self.profile_numbers = None
|
|
995
|
+
self.model_numbers = None
|
|
996
|
+
self.profile_dict = None
|
|
997
|
+
self.read_logs()
|
|
998
|
+
|
|
999
|
+
def read_logs(self):
|
|
1000
|
+
"""Read (or re-read) data from the history and profile index.
|
|
1001
|
+
|
|
1002
|
+
Reads in `self.history_path` and `self.index_file` for use in getting
|
|
1003
|
+
history data and profile information. This is automatically called at
|
|
1004
|
+
instantiation, but can be recalled by the user if for some reason the
|
|
1005
|
+
data needs to be refreshed (for instance, after changing some of the
|
|
1006
|
+
reader methods to read in specially-formatted output.)
|
|
1007
|
+
|
|
1008
|
+
Returns
|
|
1009
|
+
-------
|
|
1010
|
+
None
|
|
1011
|
+
|
|
1012
|
+
Note
|
|
1013
|
+
----
|
|
1014
|
+
This, if called after initialization, will empty `self.profile_dict`,
|
|
1015
|
+
erasing all memo-ized profiles.
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
self.history = MesaData(self.history_path)
|
|
1019
|
+
self.history_data = self.history
|
|
1020
|
+
self.profiles = MesaProfileIndex(self.index_path)
|
|
1021
|
+
self.profile_numbers = self.profiles.profile_numbers
|
|
1022
|
+
self.model_numbers = self.profiles.model_numbers
|
|
1023
|
+
self.profile_dict = dict()
|
|
1024
|
+
|
|
1025
|
+
def have_profile_with_model_number(self, m_num):
|
|
1026
|
+
"""Checks to see if a model number has a corresponding profile number.
|
|
1027
|
+
|
|
1028
|
+
Parameters
|
|
1029
|
+
----------
|
|
1030
|
+
m_num : int
|
|
1031
|
+
model number to be checked
|
|
1032
|
+
|
|
1033
|
+
Returns
|
|
1034
|
+
-------
|
|
1035
|
+
bool
|
|
1036
|
+
True if the model number is in `self.model_numbers`, otherwise
|
|
1037
|
+
False.
|
|
1038
|
+
"""
|
|
1039
|
+
return self.profiles.have_profile_with_model_number(m_num)
|
|
1040
|
+
|
|
1041
|
+
def have_profile_with_profile_number(self, p_num):
|
|
1042
|
+
"""Checks to see if a given number is a valid profile number.
|
|
1043
|
+
|
|
1044
|
+
Parameters
|
|
1045
|
+
----------
|
|
1046
|
+
p_num : int
|
|
1047
|
+
profile number to be checked
|
|
1048
|
+
|
|
1049
|
+
Returns
|
|
1050
|
+
-------
|
|
1051
|
+
bool
|
|
1052
|
+
True if profile number is in `self.profile_numbers`, otherwise
|
|
1053
|
+
False.
|
|
1054
|
+
"""
|
|
1055
|
+
return self.profiles.have_profile_with_profile_number(p_num)
|
|
1056
|
+
|
|
1057
|
+
def profile_with_model_number(self, m_num):
|
|
1058
|
+
"""Converts a model number to a corresponding profile number
|
|
1059
|
+
|
|
1060
|
+
Parameters
|
|
1061
|
+
----------
|
|
1062
|
+
m_num : int
|
|
1063
|
+
model number to be converted
|
|
1064
|
+
|
|
1065
|
+
Returns
|
|
1066
|
+
-------
|
|
1067
|
+
int
|
|
1068
|
+
profile number that corresponds to `m_num`.
|
|
1069
|
+
"""
|
|
1070
|
+
return self.profiles.profile_with_model_number(m_num)
|
|
1071
|
+
|
|
1072
|
+
def model_with_profile_number(self, p_num):
|
|
1073
|
+
"""Converts a profile number to a corresponding model number
|
|
1074
|
+
|
|
1075
|
+
Parameters
|
|
1076
|
+
----------
|
|
1077
|
+
p_num : int
|
|
1078
|
+
profile number to be converted
|
|
1079
|
+
|
|
1080
|
+
Returns
|
|
1081
|
+
-------
|
|
1082
|
+
int
|
|
1083
|
+
model number that corresponds to `p_num`.
|
|
1084
|
+
"""
|
|
1085
|
+
return self.profiles.model_with_profile_number(p_num)
|
|
1086
|
+
|
|
1087
|
+
def profile_data(self, model_number=-1, profile_number=-1):
|
|
1088
|
+
"""Generate or retrieve MesaData from a model or profile number.
|
|
1089
|
+
|
|
1090
|
+
If both a model number and a profile number is given, the model number
|
|
1091
|
+
takes precedence. If neither are given, the default is to return a
|
|
1092
|
+
MesaData object of the last profile (biggest model number). In either
|
|
1093
|
+
case, this generates (if it doesn't already exist) or retrieves (if it
|
|
1094
|
+
has already been generated and memoized) a MesaData object from the
|
|
1095
|
+
corresponding profile data.
|
|
1096
|
+
|
|
1097
|
+
Parameters
|
|
1098
|
+
----------
|
|
1099
|
+
model_number : int, optional
|
|
1100
|
+
model number for the profile MesaData object desired. Default is
|
|
1101
|
+
-1, corresponding to the last model number.
|
|
1102
|
+
profile_number : int, optional
|
|
1103
|
+
profile number for the profile MesaData object desired. Default is
|
|
1104
|
+
-1, corresponding to the last model number. If both `model_number`
|
|
1105
|
+
and `profile_number` are given, `profile_number` is ignored.
|
|
1106
|
+
|
|
1107
|
+
Returns
|
|
1108
|
+
-------
|
|
1109
|
+
mesa_reader.MesaData
|
|
1110
|
+
Data for profile with desired model/profile number.
|
|
1111
|
+
"""
|
|
1112
|
+
if model_number == -1:
|
|
1113
|
+
if profile_number == -1:
|
|
1114
|
+
to_use = self.profile_numbers[-1]
|
|
1115
|
+
else:
|
|
1116
|
+
to_use = profile_number
|
|
1117
|
+
else:
|
|
1118
|
+
to_use = self.profile_with_model_number(model_number)
|
|
1119
|
+
|
|
1120
|
+
if to_use in self.profile_dict:
|
|
1121
|
+
return self.profile_dict[to_use]
|
|
1122
|
+
|
|
1123
|
+
file_name = join(self.log_path,
|
|
1124
|
+
(self.profile_prefix + str(to_use) + '.' +
|
|
1125
|
+
self.profile_suffix))
|
|
1126
|
+
p = MesaData(file_name)
|
|
1127
|
+
if self.memoize_profiles:
|
|
1128
|
+
self.profile_dict[to_use] = p
|
|
1129
|
+
return p
|
|
1130
|
+
|
|
1131
|
+
def select_models(self, f, *keys):
|
|
1132
|
+
"""Yields model numbers for profiles that satisfy a given criteria.
|
|
1133
|
+
|
|
1134
|
+
Given a function `f` of various time-domain (history) variables,
|
|
1135
|
+
`*keys` (i.e., categories in `self.history.bulk_names`), filters
|
|
1136
|
+
`self.model_numbers` and returns all model numbers that satisfy the
|
|
1137
|
+
criteria.
|
|
1138
|
+
|
|
1139
|
+
Parameters
|
|
1140
|
+
----------
|
|
1141
|
+
f : function
|
|
1142
|
+
A function of the same number of parameters as strings given for
|
|
1143
|
+
`keys` that returns a boolean. Should evaluate to `True` when
|
|
1144
|
+
condition is met and `False` otherwise.
|
|
1145
|
+
keys : str
|
|
1146
|
+
Name of data categories from `self.history.bulk_names` whose values
|
|
1147
|
+
are to be used in the arguments to `f`, in the same order that they
|
|
1148
|
+
appear as arguments in `f`.
|
|
1149
|
+
|
|
1150
|
+
Returns
|
|
1151
|
+
-------
|
|
1152
|
+
numpy.ndarray
|
|
1153
|
+
Array of model numbers that have corresponding profiles where the
|
|
1154
|
+
condition given by `f` is `True`.
|
|
1155
|
+
|
|
1156
|
+
Raises
|
|
1157
|
+
------
|
|
1158
|
+
KeyError
|
|
1159
|
+
If any of the `keys` are invalid history keys.
|
|
1160
|
+
|
|
1161
|
+
Examples
|
|
1162
|
+
--------
|
|
1163
|
+
>>> l = MesaLogDir()
|
|
1164
|
+
>>> def is_old_and_bright(age, log_lum):
|
|
1165
|
+
>>> return age > 1e9 and log_lum > 3
|
|
1166
|
+
>>> m_nums = l.select_models(is_old_and_bright, 'star_age', 'log_L')
|
|
1167
|
+
|
|
1168
|
+
Here, `m_nums` will contain all model numbers that have profiles where
|
|
1169
|
+
the age is greater than a billion years and the luminosity is greater
|
|
1170
|
+
than 1000 Lsun, provided that 'star_age' and 'log_L' are in
|
|
1171
|
+
`self.history.bulk_names`.
|
|
1172
|
+
"""
|
|
1173
|
+
|
|
1174
|
+
for key in keys:
|
|
1175
|
+
if not self.history.in_data(key):
|
|
1176
|
+
raise KeyError("'" + str(key) + "' is not a valid data type.")
|
|
1177
|
+
inputs = {}
|
|
1178
|
+
for m_num in self.model_numbers:
|
|
1179
|
+
this_input = []
|
|
1180
|
+
for key in keys:
|
|
1181
|
+
this_input.append(
|
|
1182
|
+
self.history.data_at_model_number(key, m_num))
|
|
1183
|
+
inputs[m_num] = this_input
|
|
1184
|
+
mask = np.array([f(*inputs[m_num]) for m_num in self.model_numbers])
|
|
1185
|
+
return self.model_numbers[mask]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mesa-reader
|
|
3
|
+
Version: 0.3.3
|
|
4
|
+
Summary: tools for interacting with output from MESA star
|
|
5
|
+
Home-page: http://github.com/wmwolf/py_mesa_reader
|
|
6
|
+
Author: William M. Wolf
|
|
7
|
+
Author-email: wolfwm@uwec.edu
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
mesa_reader/__init__.py,sha256=SpDeAjysLBAj9NRJTkvtssxTHJQqMzobHqpZ53z70qM,41996
|
|
2
|
+
mesa_reader-0.3.3.dist-info/METADATA,sha256=_y65ZfOCGErVvRZy1_pB0JRMSkuVyJ79WZpKaR-rj_E,422
|
|
3
|
+
mesa_reader-0.3.3.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
4
|
+
mesa_reader-0.3.3.dist-info/top_level.txt,sha256=qT1Ab4eFqInGLxpMqrXQPKr8FvkTjvTTAIttqjUZ550,12
|
|
5
|
+
mesa_reader-0.3.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mesa_reader
|