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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mesa_reader