pyfortmesa usage

This page shows how to call the package from Python. Build and packaging commands live in Installation and Developing.

The MESA interface is narrow by design. It wraps selected const, chem, eos, and kap calls for code that needs repeated eos/kap evaluations. It is not a general MESA module reader; use pyMesa for that.

MESA example

Real MESA eos/kap calls require the ./mk mesa build for the shared libraries in your MESA_DIR. Build and install that package first; the normal commands are in Installation.

Then a Python script can call MESA const, chem, eos, and kap wrappers through pyfortmesa:

from pyfortmesa import mesa

mesa.set_cache_root(".")

# Path A: read one MESA-style file with both &eos and &kap namelists.
mesa.set_inlist("inlist_eos_and_kap")

# Inputs: MESA isotope names mapped to mass fractions. They must sum to 1.
mix = mesa.composition({"h1": 0.70, "he4": 0.28, "c12": 0.02})

# Inputs: temperature in K and density in g/cm^3.
T_eos = 1.0e7
rho_eos = 1.0e2
T_kap = 1.0e6
rho_kap = 1.0e-7

constants = mesa.constants()
chem_output = mesa.Chem().composition_info(mix)
eos = mesa.Eos()
kap = mesa.Kap()

# Path B: Python overrides can be used instead of, or on top of, the inlist.
kap_type2 = mesa.Kap(use_type2=True, zbase=0.02)

eos_output = eos.dt_full(T=T_eos, Rho=rho_eos, comp=mix)
rho_solve = eos.solve_rho(
    T=T_eos,
    other="lnPgas",
    other_value=eos_output["results"]["lnPgas"],
    Rho_guess=0.8 * rho_eos,
    comp=mix,
)
kap_output = kap.opacity_full(T=T_kap, Rho=rho_kap, comp=mix)
kap_type2_output = kap_type2.opacity_full(T=T_kap, Rho=rho_kap, comp=mix)

print("MESA const_def values:", constants)
print("chem output:", chem_output)
print("eos output:", eos_output["results"])
print("eos solve_rho output:", rho_solve["Rho"])
print("kap output:", kap_output)
print("kap Type2 fractions:", kap_type2_output["kap_fracs"])

mesa.shutdown()

mix is a scalar-call composition object. It stores the isotope names, matching MESA chem_id values, and one mass-fraction vector. Scalar helpers accept it as comp=mix.

Useful output fields:

mesa.constants()
  scalar real constants from MESA const_def; see modules/const.md

mesa.Chem().composition_info(...)
  xh, xhe, xz, abar, zbar, z2bar, z53bar, ye, mass_correction, sumx

mesa.Chem().composition_info_full(...)
  composition_info fields plus dabar_dx, dzbar_dx, dmc_dx

mesa.Eos().dt_full(...)
  results, d_dlnRho, d_dlnT, d_dxa

mesa.Eos().solve_rho(...)
  Rho, logRho, eos_calls, results, d_dlnRho, d_dlnT, d_dxa

mesa.Eos().solve_t(...)
  T, logT, eos_calls, results, d_dlnRho, d_dlnT, d_dxa

mesa.Eos().solve_rho_profile(...), mesa.Eos().solve_t_profile(...)
  profile arrays; results has shape (n_eos_results, nzones)

mesa.Kap().opacity_full(...)
  kappa, dlnkap_dlnRho, dlnkap_dlnT, kap_fracs, dlnkap_dxa

mesa.Kap(use_type2=True, zbase=...)
  enables MESA Type2 opacity controls for that Python object

Scalar hot loops

The ergonomic scalar helpers accept comp=mix and return dictionaries. For a hot scalar loop where batching is not possible, prepare the composition once and call the raw scalar helpers with mix.chem_id and mix.xa:

mix = mesa.composition({"h1": 0.70, "he4": 0.28, "c12": 0.02})
eos = mesa.Eos()
kap = mesa.Kap()

# Optional warmup: pays table/cache/handle setup before timing.
eos.dt_raw(1.0e7, 1.0e2, mix.chem_id, mix.xa)
kap.opacity_raw(1.0e6, 1.0e-7, mix.chem_id, mix.xa)

for T, rho in scalar_points:
    lnPgas, lnE, lnS, grad_ad, gamma1 = eos.dt_raw(T, rho, mix.chem_id, mix.xa)
    kappa, dkap_dlnrho, dkap_dlnt = kap.opacity_raw(T, rho, mix.chem_id, mix.xa)

When the same scalar loop needs both eos and kap for each point, use the fused call so eos is evaluated once and then passed to kap:

kap = mesa.Kap()

# Optional warmup.
kap.eos_kap_raw(1.0e6, 1.0e-7, mix.chem_id, mix.xa)

for T, rho in scalar_points:
    results, kappa, dkap_dlnrho, dkap_dlnt = kap.eos_kap_raw(
        T, rho, mix.chem_id, mix.xa
    )
    gamma1 = results[mesa.EOS_RESULT_NAMES.index("gamma1")]

These raw calls skip Composition validation and dictionary construction. They still cross the Python/F2PY boundary once per point, so profile batching is still preferred whenever the caller has many zones or grid points.

Profile calls

For profile work, do not call scalar eos or kap once per zone from Python. Batch the profile and let the Fortran wrapper parallelize the zone loop with OpenMP.

Set the OpenMP thread count before launching Python:

export OMP_NUM_THREADS=10
python run_microphysics.py

Use a long-lived Python process and keep setup out of the hot loop:

import numpy as np

from pyfortmesa import mesa

mesa.set_cache_root(".")
mesa.set_inlist("inlist_eos_and_kap")

isotope_names = ("h1", "he4", "c12")
chem_id_values = mesa.iso_ids(isotope_names)
kap = mesa.Kap()


def profile_xa(xa_by_zone):
    """Convert solver mass fractions to the profile wrapper layout.

    xa_by_zone has shape (nzones, species). The profile wrappers expect
    xa with shape (species, nzones).
    """
    return np.asfortranarray(np.asarray(xa_by_zone, dtype=np.float64).T)


try:
    # `model_profiles` is supplied by the caller's solver. Each item is one
    # model/profile with T, rho, and xa arrays.
    first_T, first_rho, first_xa_by_zone = model_profiles[0]

    # Optional warmup: pays table/cache/handle setup before timing a loop.
    kap.eos_kap_profile(first_T, first_rho, chem_id_values, profile_xa(first_xa_by_zone))

    for T, rho, xa_by_zone in model_profiles:
        xa = profile_xa(xa_by_zone)

        # combined eos+kap: one eos call per zone, then kap uses that state.
        output = kap.eos_kap_profile(T, rho, chem_id_values, xa, output="dict")

        lnPgas = output["results"]["lnPgas"]
        gamma1 = output["results"]["gamma1"]
        kappa = output["kappa"]
finally:
    mesa.shutdown()

Profile helpers take the composition in split form: chem_id_values gives the isotope order, and xa gives either one fixed composition with shape (species,) or zone mass fractions with shape (species, nzones).

For a fixed-composition eos profile on an arbitrary base-10 logT/logRho track, build one Composition and pass its 1D xa vector. Use input_mode="log10" when the arrays are base-10 logs:

log_rho = np.linspace(-2.0, 8.0, 1000)
log_T = np.linspace(3.0, 8.0, 1000)

mix = mesa.composition({"h1": 0.70, "he4": 0.28, "c12": 0.02})

output = mesa.Eos().dt_profile(log_T, log_rho, mix.chem_id, mix.xa, input_mode="log10")

For kap profile work with physical arrays, pass the temperature and density profiles directly:

T = np.linspace(6.0e5, 2.5e6, 1000)
rho = np.full_like(T, 1.0e-7)

output = mesa.Kap().eos_kap_profile(T, rho, mix.chem_id, mix.xa)

If the caller already stores base-10 logT and logRho, pass those arrays directly and set input_mode="log10":

log_T = np.linspace(5.8, 6.4, 1000)
log_rho = np.full_like(log_T, -7.0)

output = mesa.Kap().eos_kap_profile(log_T, log_rho, mix.chem_id, mix.xa, input_mode="log10")

Use input_mode="log" for natural-log arrays and input_mode="log10" for base-10 arrays. The older *_from_logs(...) helpers are compatibility aliases for natural-log input.

Use mesa.Eos().dt_profile(...) when only eos is needed. Use mesa.Kap().opacity_profile(...) when only opacity is needed; that path still calls eos internally for lnfree_e, eta, and derivatives required by kap_get. When both eos and kap are needed for the same zones, mesa.Kap().eos_kap_profile(...) is the preferred path because it avoids computing eos twice. The lower-level *_from_logs(...) variants remain available as compatibility aliases for MESA-style lnT and lnd columns.

Keep MESA setup out of the hot loop. The first call may initialize tables and create cache files. That work can be much more expensive than a hot profile call, so set cache/inlist controls once, reuse isotope ids, batch zones, keep the process alive, and call mesa.shutdown() only when the driver is done with MESA.

MESA eos logarithmic result values such as lnPgas are natural logs. If you solve from a target gas pressure, pass math.log(Pgas).

Control precedence:

MESA defaults
  -> optional shared inlist from mesa.set_inlist("inlist_eos_and_kap")
  -> optional Python kap overrides from mesa.Kap(...)

Call mesa.set_inlist(...) before the first eos or kap call in the Python process. Passing None clears the environment variable for future handle creation:

mesa.set_inlist(None)

Shutdown

mesa.shutdown() frees the persistent eos/kap handles owned by pyfortmesa and resets the wrapper state:

try:
    eos_output = mesa.Eos().dt(T=T_eos, Rho=rho_eos, comp=mix)
    kap_output = mesa.Kap().opacity(T=T_kap, Rho=rho_kap, comp=mix)
finally:
    mesa.shutdown()

That is the normal cleanup path. It does not unload all MESA table data because keeping the tables available is useful for repeated calls in the same Python process.

For a long-lived notebook or service, use the heavier path only when no other code in the process is using MESA:

mesa.shutdown(release_tables=True)

That also calls the MESA kap, eos, and chem shutdown routines for loaded table state.

Module reference

The module pages describe the supported wrapper surface:

docs/modules/README.md
docs/modules/const.md
docs/modules/chem.md
docs/modules/eos.md
docs/modules/kap.md

Example Scripts

Run the print-based examples from an environment where the package is installed. The MESA examples also require the ./mk mesa build:

export MESA_DIR=/path/to/current/mesa
./test
./test mesa
tests/work/run_simple_chem_eos_kap.sh
tests/mesa/run_eos_kap_arbitrary_composition.sh --with-mesa
tests/mesa/run_hydrostatic_structure.sh --with-mesa
tests/mesa/run_eos_from_saved_model.sh --with-mesa
OMP_NUM_THREADS=10 tests/mesa/run_eos_from_saved_model.sh --with-mesa
tests/mesa/run_profile_timing_suite.sh
tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep
tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep --physics eos-kap

./test is the normal test entrypoint for checks that do not call MESA. ./test mesa adds the optional MESA checks and the saved model timing suite. Both modes store raw test output under tests/test_output/, including test_summary.txt, tmp_golden_output.txt, and golden_compare.log; set PYFORTMESA_TEST_OUTPUT_DIR to choose a different output directory. The committed golden baseline for checks that do not call MESA is tests/test_output/golden/quick_test_output.txt. The MESA scripts print input and output data. The tests/work/ example is the smallest script to copy into new work. They are examples and integration checks, not MESA star models.

run_eos_from_saved_model.sh reads:

$MESA_DIR/star/test_suite/20M_pre_ms_to_core_collapse/standard_after_core_he_burn.mod

It passes lnT, lnd, isotope ids, and mass-fraction arrays to Fortran batch wrappers. The default --physics eos path reconstructs T = exp(lnT) and rho = exp(lnd) inside the eos loop, then calls eosDT_get once per zone using that zone's own xa(:, k) profile composition. The --physics kap path calls a kap profile wrapper; that wrapper calls eos internally for the electron quantities required by kap_get, then calls kap_get once per zone. The zone loops are OpenMP-parallel inside Fortran, so the preferred threaded timing run sets OMP_NUM_THREADS before launching the runner:

OMP_NUM_THREADS=10 tests/mesa/run_eos_from_saved_model.sh --with-mesa

The output field nproc is the number of Python processes, not the number of OpenMP threads. For the standard OpenMP thread sweep, leave --nproc off and use --thread-sweep. The default thread counts are 1 2 4 6 8 10:

tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep
tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep=1,2,4,8
PYFORTMESA_THREAD_COUNTS="1 3 6 12" \
  tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep

To time both profile paths in the same thread sweep:

tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep --physics eos-kap

The timing breakdown separates Python profile-array setup, isotope-id lookup, the eos profile call, the kap profile call, output collection, and shutdown. The kap profile call includes its required internal eos call; it is not a pure kap_get-only timer. The --physics eos-kap path uses a combined Fortran wrapper so eos is evaluated once per zone and the same eos result feeds kap.

For ad hoc overhead checks inside normal Python code, enable the package timing collector instead of writing an external wrapper:

with mesa.timing():
    output = mesa.Kap().eos_kap_profile(T, rho, chem_id, xa)

print(mesa.format_timing_summary(sort_by="seconds"))

The summary records high-level pyfortmesa public calls such as kap.eos_kap_profile. It does not write files unless the caller writes the summary.

For steady-state timings, keep MESA handles alive across repeated calls in one Python process:

tests/mesa/run_eos_from_saved_model.sh --with-mesa --physics eos --warmup 1 --repeat 5
tests/mesa/run_eos_from_saved_model.sh --with-mesa --physics kap --warmup 1 --repeat 5
tests/mesa/run_eos_from_saved_model.sh --with-mesa --physics eos-kap --warmup 1 --repeat 5
tests/mesa/run_eos_from_saved_model.sh --with-mesa --thread-sweep \
  --physics eos-kap --warmup 1 --repeat 5

For the standard report, use one command:

tests/mesa/run_profile_timing_suite.sh

That suite runs eos, kap, combined eos+kap, and the combined eos+kap thread sweep, then prints compact single run, global-breakdown, and thread-sweep tables. Raw per-run logs and timing JSON are temporary by default; set PYFORTMESA_PROFILE_REPORT_DIR to keep them. The wrapper defaults to warmup=1 and repeat=5; override those with PYFORTMESA_PROFILE_WARMUP and PYFORTMESA_PROFILE_REPEAT. The single run thread count is chosen from PYFORTMESA_SUMMARY_THREADS, then OMP_NUM_THREADS, then the largest sweep count.

The equivalent expanded command is:

tests/mesa/run_eos_from_saved_model.sh --with-mesa --summary-suite --warmup 1 --repeat 5

The call rate fields use only timed profile call seconds. Warmup, array setup, output collection, and shutdown are reported separately.

The script also supports --nproc, but that uses separate Python processes. Do not combine process parallelism with --thread-sweep; the runner rejects that combination to avoid mixing process and thread parallelism.

The script prints parse time, profile wall time, call counts, calls per second, public-call timing, Fortran reconstruction checks, and first/middle/last sample outputs. Its .mod comparison checks the saved model inputs: zone count, species count, round trip lnT/lnd, and mass-fraction sums. That .mod file does not contain eos result columns such as lnPgas or gamma1.