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:
numpyis inrequirements.txt. It is used for runtime arrays and the NumPy headers needed when building extension modules.buildis inrequirements.txt. It runs the standard Python package build command used by./mk.meson-pythonis inrequirements.txt. It connects Python packaging to the Meson build system throughpyproject.toml.mesonis inrequirements.txt. It configures the compiled extension modules and handles the Fortran/C build rules.ninjais inrequirements.txt. It executes the low-level build steps generated by Meson.twineis inrequirements-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:
- Call
mesa.set_cache_root(...)andmesa.set_inlist(...)once before the first eos or kap call. - Build the composition once.
mesa.composition(...)andmesa.iso_ids(...)map MESA isotope names tochem_idvalues using$MESA_DIR/data/chem_data/isotopes.data. - Create long-lived
mesa.Eos()andmesa.Kap()objects. The first call initializes MESA tables and handles; later calls reuse them in the same Python process. - 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.
- OpenMP parallelizes that Fortran zone loop when
OMP_NUM_THREADSis set before Python starts. - Kap calls require eos electron-state quantities (
lnfree_eandeta), so kap timings include the required internal eos call. When both eos and kap are needed, usemesa.Kap().eos_kap_profile(...)so eos is computed once per zone and then passed to kap. - 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