# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""mesa.py:
Classes to read MESA stellar evolution code output files (history and profile).
MESA (Modules for Experiments in Stellar Astrophysics) is a widely-used
stellar evolution code. This reader handles the standard MESA history and
profile file formats.
See: https://docs.mesastar.org/en/stable/using_mesa/output.html
:Author: Bill Wolf
"""
import numpy as np
from . import core
class MesaHeader(core.BaseHeader):
"""Reader for MESA file headers.
MESA files have a distinctive format:
- Line 0: Column indices for metadata
- Line 1: Metadata column names
- Line 2: Metadata values
- Line 3: Blank line
- Line 4: Column indices for data table
- Line 5: Column names for data table
- Line 6+: Data rows
"""
start_line = 5 # Column names are on line 5 (0-indexed, in original file)
comment = None # No comment character in MESA files
def get_cols(self, lines):
"""Initialize the header Column objects from MESA file lines.
Note: get_cols receives the ORIGINAL lines (not processed), so we use
the original line indices:
- Line 0: metadata column indices
- Line 1: metadata column names
- Line 2: metadata values
- Line 3: blank line
- Line 4: data column indices
- Line 5: data column names <- we want this
Parameters
----------
lines : list
List of table lines (original, not processed)
"""
# Convert lines to a list so we can index it
lines_list = list(lines)
if len(lines_list) < 6:
raise core.InconsistentTableError(
"MESA file must have at least 6 lines (metadata + header + data)"
)
# Line 5 (0-indexed) contains the column names
col_names_line = lines_list[5]
# Split the line to get column names
self.names = col_names_line.split()
self._set_cols_from_names()
def update_meta(self, lines, meta):
"""Extract MESA metadata from the file header.
MESA files contain metadata in the first three lines:
- Line 1: Metadata column names
- Line 2: Metadata values
These are stored in meta['table']['mesa_metadata'] as a dictionary.
"""
# Get lines as a list to access by index
lines_list = list(lines)
if len(lines_list) < 3:
return
# Parse metadata from lines 1 and 2
# Line 1: metadata column names
# Line 2: metadata values
meta_names_line = lines_list[1]
meta_values_line = lines_list[2]
# Split the lines using whitespace
meta_names = meta_names_line.split()
meta_values = meta_values_line.split()
# Create metadata dictionary
mesa_meta = {}
for name, value in zip(meta_names, meta_values):
# Strip quotes from string values
if value.startswith('"') and value.endswith('"'):
value = value.strip('"')
# Try to convert to number if possible
else:
try:
# Try integer first
value = int(value)
except ValueError:
try:
# Try float
value = float(value)
except ValueError:
# Keep as string
pass
mesa_meta[name] = value
# Store in table metadata
meta.setdefault("table", {})["header"] = mesa_meta
class MesaData(core.BaseData):
"""Reader for MESA data section.
Data starts at line 5 (0-indexed) after blank lines are removed.
After process_lines removes the blank line 3, the structure is:
- Line 0: metadata column indices
- Line 1: metadata column names
- Line 2: metadata values
- Line 3: data column indices (line 4 in original file)
- Line 4: data column names (line 5 in original file)
- Line 5: first data row (line 6 in original file)
"""
start_line = 5
comment = None # No comment character
delimiter = None # Whitespace-delimited
[docs]
class Mesa(core.BaseReader):
"""MESA stellar evolution code output format.
MESA (Modules for Experiments in Stellar Astrophysics) is a widely-used
one-dimensional stellar evolution code. This reader handles both history
files (evolution over time) and profile files (spatial profiles at a
single timestep). It does NOT handle model (.mod) files or output from
associated packages like GYRE, adipls, or Stella.
The MESA format consists of:
- Lines 0-2: Metadata section with column indices, names, and values
- Line 3: Blank line
- Line 4: Column indices for main data table
- Line 5: Column names for main data table
- Line 6+: Data rows (whitespace-delimited)
The metadata from lines 1-2 is stored in the table's ``meta['header']``
dictionary.
For history files, this reader automatically detects and removes restart
artifacts. When a MESA run is restarted from an earlier model, the history
file will contain duplicate model numbers. This reader removes the earlier
instances so that the resulting table has monotonically increasing model
numbers suitable for analysis and plotting.
Example::
1 2
version_number compiler
"r24.03.1" "gfortran"
1 2 3
model_number num_zones star_age
1 1004 1.00000000000E-05
2 1025 2.20000000000E-05
Example usage::
>>> from astropy.table import Table
>>> from astropy.utils.data import get_pkg_data_filename
>>> # Find the location of an example file included in astropy
>>> mesa_file = get_pkg_data_filename('data/history_mesa.data',
... package='astropy.io.ascii.tests')
>>> t = Table.read(mesa_file, format='ascii.mesa')
>>> print(t.meta['header']['version_number'])
r24.03.1
>>> t['model_number', 'star_age'][:3]
<Table length=3>
model_number star_age
int64 float64
------------ ----------------------
1 1e-05
2 2.2e-05
3 3.6400000000000004e-05
"""
_format_name = "mesa"
_description = "MESA stellar evolution code output"
_io_registry_can_write = False
header_class = MesaHeader
data_class = MesaData
def __init__(self, remove_restart_rows=True):
"""Initialize MESA reader.
Parameters
----------
remove_restart_rows : bool, optional
If True (default), automatically remove restart artifacts from history
files by detecting non-monotonic model numbers. Set to False to keep
all rows including restart artifacts.
"""
super().__init__()
self.remove_restart_rows = remove_restart_rows
[docs]
def read(self, table):
"""Read input data into a Table and return the result.
Parameters
----------
table : str, file-like, list
Input table data
Returns
-------
out : `~astropy.table.Table`
Output table with restart artifacts removed (if applicable)
"""
out = super().read(table)
# Check if this is a history file and remove restarts if requested
if self.remove_restart_rows and "model_number" in out.colnames:
out = self._remove_restart_rows(out)
return out
def _remove_restart_rows(self, table):
"""
Remove rows from history files that correspond to restarts.
When MESA restarts from an earlier model, the history file contains
duplicate model numbers. We iterate backwards through the model_number
column and remove rows where the model number is >= the minimum seen
so far (going backwards). This identifies the "old" evolution that was
restarted.
Example:
Model numbers: [1, 2, 3, 4, 5, 3, 4, 5, 6, 7]
Going backwards:
- i=9: model=7, min_seen=7, keep
- i=8: model=6, min_seen=6, keep
- i=7: model=5, min_seen=5, keep
- i=6: model=4, min_seen=4, keep
- i=5: model=3, min_seen=3, keep
- i=4: model=5, 5 >= 3, REMOVE
- i=3: model=4, 4 >= 3, REMOVE
- i=2: model=3, 3 >= 3, REMOVE
- i=1: model=2, 2 < 3, min_seen=2, keep
- i=0: model=1, 1 < 2, min_seen=1, keep
Result: [1, 2, 3, 4, 5, 6, 7]
Parameters
----------
table : `~astropy.table.Table`
Input table with potential restart artifacts
Returns
-------
table : `~astropy.table.Table`
Table with restart artifacts removed
"""
model_number = table["model_number"]
# are looking at the reversed array to get the last occurrence
smallest_seen = np.minimum.accumulate(model_number[::-1])
_, reverse_index = np.unique(smallest_seen[:], return_index=True)
# Transform the index to work for the original, non-reversed table
keep = len(table) - reverse_index - 1
return table[keep]