Developing

This page collects the common development commands. If the pyfortmesa conda environment is already active, use the shorter command. If not, use the conda run -n pyfortmesa ... form.

Environment setup

Install the development requirements:

python -m pip install -r requirements-dev.txt

or, without activating first:

conda run -n pyfortmesa python -m pip install -r requirements-dev.txt

Install the editable checkout when you want imports to follow local source edits:

python -m pip install --no-build-isolation --editable .

or, without activating first:

conda run -n pyfortmesa python -m pip install --no-build-isolation --editable .

Check the import:

python -m pyfortmesa

or, without activating first:

conda run -n pyfortmesa python -m pyfortmesa

The editable install is useful while editing the package. The default ./test command runs against src/ directly, so it does not require an editable install.

requirements-dev.txt includes requirements.txt, then adds the docs, testing, and publishing tools used while developing the package.

Build system requirements

These are the package build tools listed in requirements.txt and requirements-dev.txt:

  • numpy is in requirements.txt. It is used for runtime arrays and the NumPy headers needed when building extension modules.
  • build is in requirements.txt. It runs the standard Python package build command used by ./mk.
  • meson-python is in requirements.txt. It connects Python packaging to the Meson build system through pyproject.toml.
  • meson is in requirements.txt. It configures the compiled extension modules and handles the Fortran/C build rules.
  • ninja is in requirements.txt. It executes the low-level build steps generated by Meson.
  • twine is in requirements-dev.txt. It checks or uploads release files when publishing to PyPI.

requirements.txt is enough to build and install the ./mk mesa package. requirements-dev.txt includes it, then adds docs, testing, and release tools.

Typical MESA call path

The build tools above are not in the hot path. They are used to build the extension modules. In a running Python job, a typical fast eos/kap path is:

  1. Call mesa.set_cache_root(...) and mesa.set_inlist(...) once before the first eos or kap call.
  2. Build the composition once. mesa.composition(...) and mesa.iso_ids(...) map MESA isotope names to chem_id values using $MESA_DIR/data/chem_data/isotopes.data.
  3. Create long-lived mesa.Eos() and mesa.Kap() objects. The first call initializes MESA tables and handles; later calls reuse them in the same Python process.
  4. For scalar work, Python crosses into one F2PY wrapper for one thermodynamic state. For profile work, Python passes the whole profile arrays once, and the zone loop runs inside the Fortran wrapper.
  5. OpenMP parallelizes that Fortran zone loop when OMP_NUM_THREADS is set before Python starts.
  6. Kap calls require eos electron-state quantities (lnfree_e and eta), so kap timings include the required internal eos call. When both eos and kap are needed, use mesa.Kap().eos_kap_profile(...) so eos is computed once per zone and then passed to kap.
  7. Call mesa.shutdown() once when the driver is done with MESA. Do not put it inside a repeated-call loop.

The main source files for this path are:

src/pyfortmesa/mesa.py
src/pyfortmesa/mesa_support.py
src/pyfortmesa/fortran/eos/mesa_eos_public.f90
src/pyfortmesa/fortran/kap/mesa_kap_public.f90
tests/mesa/eos_from_saved_model.py

Output schemas

Use mesa.output_schema(...) when you want the expected eos/kap output layout without making a MESA call. The helper returns plain Python rows with column, path, units, shape, and comment keys. This is useful when deciding the columns for a DataFrame or checking nested output dictionaries.

from pyfortmesa import mesa

for row in mesa.output_schema("eos_kap_profile"):
    print(row["column"], row["units"], row["shape"])

For an interactive, comment-style summary:

print(mesa.format_output_schema("eos_kap_profile"))

which prints entries like:

T               # units=K; shape=(nzones,); temperature
Rho             # units=g/cm^3; shape=(nzones,); density
results.lnPgas  # units=ln(dyn/cm^2); shape=(nzones,); natural log gas pressure
kappa           # units=cm^2/g; shape=(nzones,); Rosseland mean opacity

For scalar full-output helpers, pass isotope names to expand composition derivative columns:

mesa.output_columns("kap_opacity_full", species=("h1", "he4", "c12"))
mesa.format_output_schema("eos_dt_full", species=("h1", "he4", "c12"))

Supported schema names are:

mesa.output_schema_names()
mesa.output_schema_names(include_aliases=True)

MESA handle lifecycle

MESA eos and kap handles are integer slots into MESA-owned handle arrays. MESA defines those arrays in its own source:

$MESA_DIR/eos/public/eos_def.f90 -> eos_handles(max_eos_handles)
$MESA_DIR/kap/public/kap_def.f90 -> kap_handles(max_kap_handles)

The integer handle selects one EoS_General_Info or Kap_General_Info slot. That slot stores per-caller controls read from an inlist or set by code. MESA uses this so different callers can have different control state in the same process.

pyfortmesa currently keeps one persistent handle per wrapper module, not one handle per Python object:

src/pyfortmesa/fortran/eos/mesa_eos_public.f90
  eos_handle_store -> pyfortmesa's eos handle

src/pyfortmesa/fortran/kap/mesa_kap_public.f90
  kap_handle_store -> pyfortmesa's kap handle
  eos_handle_store -> eos handle owned by the kap wrapper

The first eos call goes through ensure_eos_handle, initializes MESA if needed, and allocates eos_handle_store. The first kap call goes through ensure_kap_handles, initializes MESA if needed, and allocates both kap_handle_store and the kap-owned eos_handle_store. Later calls reuse those saved integers.

The Python classes are lighter than MESA handles:

kap_type1 = mesa.Kap(use_type2=False)
kap_type2 = mesa.Kap(use_type2=True, zbase=0.02)

Those two Python objects do not allocate two MESA kap handles. They store two sets of Python controls. Before each wrapper call, pyfortmesa applies the selected object's controls to the one saved Fortran kap_handle_store with apply_kap_controls.

This is different from MESA's kap/test/src/sample_kap.f90, which allocates two MESA kap handles so it can evaluate Type1 and Type2 opacities side by side in one loop. Normal MESA/star usage usually has one s% kap_handle whose controls may enable Type2. Type2 itself does not require two handles; MESA's kap_get does the Type1/Type2 blending from the controls on the handle.

The current public pyfortmesa API therefore cannot call two independent MESA kap handles at the same time. Supporting that would need a wrapper change, such as storing more than one Fortran handle or exposing explicit handle allocation, freeing, and handle-indexed call routines.

mesa.shutdown() frees pyfortmesa's saved handles but leaves MESA's loaded tables available:

mesa.shutdown()
  -> free_kap_handle(kap_handle_store)
  -> free_eos_handle(kap wrapper eos_handle_store)
  -> free_eos_handle(eos wrapper eos_handle_store)
  -> reset pyfortmesa saved handles to -1

After that, another eos or kap call can allocate fresh handles again. This is why mesa.shutdown() belongs at the end of a driver, not inside a repeated-call loop.

mesa.shutdown(release_tables=True) is heavier. It does the handle cleanup above and also calls the MESA shutdown routines for loaded table state:

kap_shutdown()
eos_shutdown()
chem_shutdown()

Use release_tables=True only when no other code in the Python process is using MESA. Releasing tables can make a later call pay initialization and table-loading costs again, and it is not needed for normal end-of-driver cleanup.

Package builds

A wheel is Python's built install file, with a .whl suffix.

./mk builds the plain package. That is enough for imports, docs, and the quick checks that do not call MESA. It does not build the compiled MESA extension modules, so it cannot run MESA eos or kap calls.

./mk mesa builds the pyfortmesa wheel that links to MESA. Use this before running Python code that calls MESA const, chem, eos, or kap, and before ./test mesa. That build is local because the compiled extension modules link against the MESA libraries in your MESA_DIR.

Build the plain package:

./mk

or, without activating first:

conda run -n pyfortmesa ./mk

Install the newest local wheel from dist/:

./install

or, without activating first:

conda run -n pyfortmesa ./install

Clean local build output:

./clean

or, without activating first:

conda run -n pyfortmesa ./clean

./clean removes build/, dist/, .mesonpy-*, caches, and Python bytecode.

Release package checks

Most users do not need sdist. A source distribution is the source archive used for release checks and future PyPI publishing. It is useful when checking that a source release contains the files needed to rebuild the package. It is not needed for normal local work, MESA calls, or ./test mesa.

Build only the source archive:

./mk sdist

or, without activating first:

conda run -n pyfortmesa ./mk sdist

Build both the wheel and source archive for a release check:

./mk all

or, without activating first:

conda run -n pyfortmesa ./mk all

./mk mesa build

Only use this path when Python code needs to call MESA const, chem, eos, or kap, or when you want to run ./test mesa. A plain ./mk build installs the package, but it cannot call MESA. Build MESA separately first.

The build expects:

MESASDK_ROOT -> the MESA SDK location, for compiler and runtime tools
MESA_DIR     -> the MESA checkout to link against
USE_SHARED   -> YES, so MESA provides shared module libraries

A current MESA development tree usually has the shared-library build layout. For a release tree, check $MESA_DIR/utils/makefile_header; if it says USE_SHARED = NO, set USE_SHARED = YES, then rebuild MESA with ./clean and ./install. Static-only MESA builds are not supported by this package.

The build also expects four MESA pkg-config files. These are small .pc text files that tell the compiler where the MESA shared libraries and module files are:

mesa-const.pc
mesa-chem.pc
mesa-eos.pc
mesa-kap.pc

The helper searches these layouts under MESA_DIR:

$MESA_DIR/lib/pkgconfig/
$MESA_DIR/build/lib/pkgconfig/
$MESA_DIR/build/<build-name>/lib/pkgconfig/

The exact <build-name> is chosen by the MESA build system. To see what this checkout finds, run:

python tools/mesa_pkg_config.py path

You do not normally set PKG_CONFIG_PATH by hand for this package. ./mk mesa runs python tools/mesa_pkg_config.py path, sets PKG_CONFIG_PATH from that output, and then builds with -Dwith_mesa=true.

The MESA SDK setup and MESA_DIR are separate things. Sourcing the MESA SDK sets up compilers and runtime libraries. Setting MESA_DIR tells this package which MESA checkout to inspect and link against.

With an active environment:

conda activate pyfortmesa
python -m pip install -r requirements-dev.txt
export MESASDK_ROOT=/Applications/mesasdk
source "$MESASDK_ROOT/bin/mesasdk_init.sh"
export MESA_DIR=/path/to/current/mesa
./clean
./mk mesa
./install

or, without activating first:

conda run -n pyfortmesa python -m pip install -r requirements-dev.txt
export MESASDK_ROOT=/Applications/mesasdk
source "$MESASDK_ROOT/bin/mesasdk_init.sh"
export MESA_DIR=/path/to/current/mesa
conda run -n pyfortmesa ./clean
conda run -n pyfortmesa ./mk mesa
conda run -n pyfortmesa ./install

./clean removes old local build output before the MESA build. That is useful when switching between builds or changing MESA_DIR.

A useful preflight check is:

echo $MESA_DIR
python tools/mesa_pkg_config.py path
PKG_CONFIG_PATH=$(python tools/mesa_pkg_config.py path) \
  pkg-config --cflags --libs mesa-const mesa-chem mesa-eos mesa-kap

The optional MESA extensions are declared in meson.build by mesa_wrapper_specs. Each entry names one extension module, its Fortran source, the pkg-config packages it needs, and the Fortran symbols exposed through f2py. When adding a new wrapper routine to an existing module, add its Fortran subroutine name to that module's symbols list. Add a new table entry only when there is a new compiled extension module.

Docs

Build the strict local site:

python -m mkdocs build --clean --strict --site-dir site

or, without activating first:

conda run -n pyfortmesa python -m mkdocs build --clean --strict --site-dir site

Serve the docs locally:

python -m mkdocs serve

or, without activating first:

conda run -n pyfortmesa python -m mkdocs serve

The public docs URL is:

https://debraheem.github.io/pyfortmesa/

Tests

Run checks that do not call MESA:

./test

or, without activating first:

conda run -n pyfortmesa ./test

Run MESA checks after installing the ./mk mesa build:

./test mesa

or, without activating first:

conda run -n pyfortmesa ./test mesa

Run the profile timing suite directly:

tests/mesa/run_profile_timing_suite.sh

or, without activating first:

conda run -n pyfortmesa tests/mesa/run_profile_timing_suite.sh

The quick ./test path does not require ./mk mesa. MESA tests require MESA_DIR and an installed ./mk mesa build.

PyPI release

PyPI should get the plain Python release files, not a local ./mk mesa wheel. The ./mk mesa wheel is tied to one MESA checkout and compiler setup. For PyPI, build the plain wheel and the source archive from the committed release tree.

PyPI does not allow replacing an uploaded file with the same version number. If the version has already been uploaded, bump the version before trying again.

The plain build uses with_mesa=false, so it installs only Python files and should produce a pure py3-none-any wheel for PyPI. The ./mk mesa build uses with_mesa=true; that is the local compiled wheel for MESA calls and should not be uploaded to PyPI.

The automatic path is the normal release path. The workflow file is:

.github/workflows/pypi.yml

It runs when a tag like v0.6.0 or v0.6.1 is pushed. The workflow runs ./test, builds with ./mk all, checks the distributions with twine, and uploads dist/* to PyPI using the GitHub secret PYPI_API_TOKEN.

Use release branches for PyPI releases. A branch like release/v0.6 is an editable maintenance line. A tag like v0.6.0 points to one exact commit and is what triggers the PyPI workflow.

For the first 0.6.0 release, make the release branch from a clean main, then point the tag at that branch:

git switch main
git pull --ff-only origin main
git switch -c release/v0.6
git push origin release/v0.6
git tag v0.6.0 release/v0.6
git push origin v0.6.0

That tag push triggers .github/workflows/pypi.yml. No branch trigger is needed for publishing. Tags are global refs, so a pushed v* tag triggers the PyPI workflow whether the tagged commit is on main or on release/v0.6.

For a later fix on the same release line, commit to release/v0.6, bump the version in pyproject.toml, then make a new tag:

git switch release/v0.6
# edit files and bump pyproject.toml to 0.6.1
git commit -am "release 0.6.1"
git tag v0.6.1 release/v0.6
git push origin release/v0.6
git push origin v0.6.1

Do not move a tag after PyPI has accepted that version. PyPI files are immutable by version; bump the version instead. After the release tag succeeds, switch back to main for normal development. Keep release/v0.6 for 0.6.x patches only.

Use a PyPI API token stored in GitHub, not in the repo. The simplest setup is a repository secret named PYPI_API_TOKEN. The workflow also names a GitHub environment called pypi, so an environment secret with the same name works too if you want an approval step before upload.

The manual path is useful for the first upload or for debugging the release files locally. Before uploading manually, make sure the version in pyproject.toml is the version you want to publish, main is pushed, and the matching release tag is pushed.

With an active environment:

conda activate pyfortmesa
python -m pip install -r requirements-dev.txt
./clean
./mk all
python -m twine check dist/*
python -m twine upload -u __token__ dist/*

or, without activating first, do the build and check with conda run:

conda run -n pyfortmesa python -m pip install -r requirements-dev.txt
conda run -n pyfortmesa ./clean
conda run -n pyfortmesa ./mk all
conda run -n pyfortmesa python -m twine check dist/*

For the final upload, use an active environment so the token prompt is plain:

conda activate pyfortmesa
python -m twine upload -u __token__ dist/*

When twine asks for the password, paste the PyPI API token. Do not put the token in the command line or in a committed file.

After upload, check the package page and test the install in a clean environment:

python -m pip install --upgrade pyfortmesa
python -m pyfortmesa